Browse Source

Pass original logical size to render targets (#21380)

* Pass original logical size to render targets

* Change FromSizeCeiling method to internal access

* api diff again?
pull/21394/head
Nikita Tsukanov 5 months ago
committed by GitHub
parent
commit
be6bc063f1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      api/Avalonia.nupkg.xml
  2. 21
      src/Avalonia.Base/PixelSize.cs
  3. 7
      src/Avalonia.Base/Platform/IRenderTarget.cs
  4. 2
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  5. 2
      src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs
  6. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  7. 2
      src/Avalonia.Base/composition-schema.xml
  8. 38
      tests/Avalonia.Base.UnitTests/PixelSizeTests.cs
  9. 6
      tests/Avalonia.Base.UnitTests/Rendering/CompositorLifetimeTests.cs
  10. 2
      tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs

12
api/Avalonia.nupkg.xml

@ -19,6 +19,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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}})</Target>
@ -37,4 +43,10 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>

21
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;
/// <summary>
/// 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
/// </summary>
/// <param name="size">The logical size.</param>
/// <param name="scale">The scaling factor.</param>
/// <returns>The pixel size that contains the logical size at the given scale.</returns>
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);
}
/// <summary>
/// Converts a <see cref="Size"/> to device pixels using the specified scaling factor.

7
src/Avalonia.Base/Platform/IRenderTarget.cs

@ -32,6 +32,11 @@ namespace Avalonia.Platform
/// </summary>
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))
{
}
}
}
}

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

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

4
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);
/// <summary>
/// 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;

2
src/Avalonia.Base/composition-schema.xml

@ -58,7 +58,7 @@
<Property Name="DebugOverlays" Type="RendererDebugOverlays"/>
<Property Name="LastLayoutPassTiming" Type="LayoutPassTiming" Internal="true"/>
<Property Name="Scaling" Type="double"/>
<Property Name="PixelSize" Type="PixelSize" />
<Property Name="Size" Type="Size" />
</Object>
<KeyFrameAnimation Name="Scalar" Type="float"/>
<KeyFrameAnimation Name="Double" Type="double"/>

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

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

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

Loading…
Cancel
Save