diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 78f8b13477..fd711d37c0 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -19,6 +19,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Input.PointerEventArgs.#ctor(Avalonia.Interactivity.RoutedEvent,System.Object,Avalonia.Input.IPointer,Avalonia.Visual,Avalonia.Point,System.UInt64,Avalonia.Input.PointerPointProperties,Avalonia.Input.KeyModifiers,System.Lazy{System.Collections.Generic.IReadOnlyList{Avalonia.Input.Raw.RawPointerPoint}})
@@ -37,4 +43,10 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
\ No newline at end of file
diff --git a/src/Avalonia.Base/PixelSize.cs b/src/Avalonia.Base/PixelSize.cs
index 797988e659..a1699f565f 100644
--- a/src/Avalonia.Base/PixelSize.cs
+++ b/src/Avalonia.Base/PixelSize.cs
@@ -203,6 +203,27 @@ namespace Avalonia
(int)Math.Round(size.Width * scale),
(int)Math.Round(size.Height * scale));
+ private const double FromSizeCeilingEpsilon = 1e-6;
+
+ ///
+ /// Converts logical size back to PixelSize and rounds it up with a small epsilon to avoid having
+ /// extra pixels when doing platform pixel size -> logical size -> pixel size conversions
+ ///
+ /// The logical size.
+ /// The scaling factor.
+ /// The pixel size that contains the logical size at the given scale.
+ internal static PixelSize FromSizeCeiling(Size size, double scale) => new PixelSize(
+ CeilWithEpsilon(size.Width * scale),
+ CeilWithEpsilon(size.Height * scale));
+
+ private static int CeilWithEpsilon(double value)
+ {
+ var rounded = Math.Round(value);
+ if (Math.Abs(value - rounded) < FromSizeCeilingEpsilon)
+ return (int)rounded;
+ return (int)Math.Ceiling(value);
+ }
+
///
/// Converts a to device pixels using the specified scaling factor.
diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs
index 2e7d56405a..b9da6a35b6 100644
--- a/src/Avalonia.Base/Platform/IRenderTarget.cs
+++ b/src/Avalonia.Base/Platform/IRenderTarget.cs
@@ -32,6 +32,11 @@ namespace Avalonia.Platform
///
PlatformRenderTargetState PlatformRenderTargetState => PlatformRenderTargetState.Ready;
- public record struct RenderTargetSceneInfo(PixelSize Size, double Scaling);
+ public record struct RenderTargetSceneInfo(PixelSize Size, double Scaling, Size LogicalSize)
+ {
+ public RenderTargetSceneInfo(PixelSize size, double scaling) : this(size, scaling, size.ToSize(scaling))
+ {
+ }
+ }
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
index c8efb9b931..2eca7e391f 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
@@ -173,7 +173,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
_dirty.Clear();
_recalculateChildren.Clear();
- CompositionTarget.PixelSize = PixelSize.FromSizeRounded(_root.ClientSize, _root.RenderScaling);
+ CompositionTarget.Size = _root.ClientSize;
CompositionTarget.Scaling = _root.RenderScaling;
var commit = _compositor.RequestCompositionBatchCommitAsync();
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs
index 9873fe6ff3..52acf4ac33 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs
@@ -109,7 +109,7 @@ internal class CompositionTargetOverlays
targetContext.Transform = Matrix.CreateScale(_target.Scaling, _target.Scaling);
using (var immediate = new ImmediateDrawingContext(targetContext, false))
- DrawOverlays(immediate, hasLayer, _target.PixelSize.ToSize(_target.Scaling));
+ DrawOverlays(immediate, hasLayer, _target.Size);
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
index c7e0de469c..7142e47630 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
@@ -39,6 +39,8 @@ namespace Avalonia.Rendering.Composition.Server
public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public int RenderedVisuals { get; set; }
public int VisitedVisuals { get; set; }
+
+ internal PixelSize PixelSize => Avalonia.PixelSize.FromSizeCeiling(Size, Scaling);
///
/// Returns true if the target is enabled and has pending work but its render target was not ready.
@@ -197,7 +199,7 @@ namespace Avalonia.Rendering.Composition.Server
|| !(_renderTarget.Properties.RetainsPreviousFrameContents
&& _renderTarget.Properties.IsSuitableForDirectRendering);
- using (var renderTargetContext = _renderTarget.CreateDrawingContext(new(PixelSize, Scaling), out var properties))
+ using (var renderTargetContext = _renderTarget.CreateDrawingContext(new(PixelSize, Scaling, Size), out var properties))
using (var renderTiming = Diagnostic.BeginCompositorRenderPass())
{
var fullRedraw = false;
diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml
index defe02769c..207f80647b 100644
--- a/src/Avalonia.Base/composition-schema.xml
+++ b/src/Avalonia.Base/composition-schema.xml
@@ -58,7 +58,7 @@
-
+
diff --git a/tests/Avalonia.Base.UnitTests/PixelSizeTests.cs b/tests/Avalonia.Base.UnitTests/PixelSizeTests.cs
index b0c002b5d7..cc80cf51a5 100644
--- a/tests/Avalonia.Base.UnitTests/PixelSizeTests.cs
+++ b/tests/Avalonia.Base.UnitTests/PixelSizeTests.cs
@@ -74,4 +74,42 @@ public class PixelSizeTests
null
];
}
+
+ [Theory]
+ [InlineData(10, 1.0, 10)]
+ [InlineData(10, 1.25, 13)]
+ [InlineData(10, 1.5, 15)]
+ [InlineData(10, 1.75, 18)]
+ [InlineData(10, 2.0, 20)]
+ [InlineData(10, 1.125, 12)]
+ [InlineData(8, 1.5, 12)]
+ [InlineData(0, 1.5, 0)]
+ [InlineData(1, 2.5, 3)]
+ public void FromSizeCeiling_Computes_Expected_Pixels(int logical, double scale, int expected)
+ {
+ var pixel = PixelSize.FromSizeCeiling(new Size(logical, logical), scale);
+ Assert.Equal(expected, pixel.Width);
+ Assert.Equal(expected, pixel.Height);
+ }
+
+ [Theory]
+ [InlineData(1.5)]
+ [InlineData(2.0)]
+ [InlineData(3.0)]
+ public void FromSizeCeiling_Snaps_When_Within_Epsilon(double scale)
+ {
+ // Pick a logical size where logical * scale is an exact integer; perturbing it by a tiny
+ // amount in either direction must still produce that integer (no spurious +1 from ceiling).
+ const int logical = 10;
+ var exact = logical * scale;
+ var below = exact - 1e-9;
+ var above = exact + 1e-9;
+ var roundedBelow = below / scale;
+ var roundedAbove = above / scale;
+
+ var p1 = PixelSize.FromSizeCeiling(new Size(roundedBelow, roundedBelow), scale);
+ var p2 = PixelSize.FromSizeCeiling(new Size(roundedAbove, roundedAbove), scale);
+ Assert.Equal((int)exact, p1.Width);
+ Assert.Equal((int)exact, p2.Width);
+ }
}
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorLifetimeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorLifetimeTests.cs
index 03c84e664a..4b5c6486cb 100644
--- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorLifetimeTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorLifetimeTests.cs
@@ -17,7 +17,7 @@ public class CompositorLifetimeTests : CompositorTestsBase
var compositionTarget = ((CompositingRenderer)presentationSource.Renderer).CompositionTarget;
Assert.True(compositionTarget.IsEnabled);
- Assert.Equal(new PixelSize(200, 200), compositionTarget.PixelSize);
+ Assert.Equal(new Size(200, 200), compositionTarget.Size);
// Stop rendering and invalidate a visual: this should not result in an update
services.TopLevel.StopRendering();
@@ -25,12 +25,12 @@ public class CompositorLifetimeTests : CompositorTestsBase
services.TopLevel.InvalidateVisual();
services.RunJobs();
- Assert.Equal(new PixelSize(200, 200), compositionTarget.PixelSize);
+ Assert.Equal(new Size(200, 200), compositionTarget.Size);
// Check that restarting rendering re-queues the pending invalidation
services.TopLevel.StartRendering();
services.RunJobs();
- Assert.Equal(new PixelSize(300, 300), compositionTarget.PixelSize);
+ Assert.Equal(new Size(300, 300), compositionTarget.Size);
}
}
diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
index 06df626857..7e3657d95a 100644
--- a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
+++ b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
@@ -55,7 +55,7 @@ public class CompositionTargetUpdateOnly : IDisposable
_compositor = new Compositor(RenderLoop.FromTimer(new Timer()), null, true, new ManualScheduler(), true,
Dispatcher.UIThread, null);
_target = _compositor.CreateCompositionTarget(() => [new NullFramebuffer()]);
- _target.PixelSize = new PixelSize(1000, 1000);
+ _target.Size = new Size(1000, 1000);
_target.Scaling = 1;
var root = _compositor.CreateContainerVisual();
root.Size = new Vector(1000, 1000);