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);