diff --git a/.github/workflows/wpf-shader-effect-reference.yml b/.github/workflows/wpf-shader-effect-reference.yml index d04b4000d..aefb35d54 100644 --- a/.github/workflows/wpf-shader-effect-reference.yml +++ b/.github/workflows/wpf-shader-effect-reference.yml @@ -62,6 +62,11 @@ jobs: $value = Get-Content -LiteralPath $receipt -Raw | ConvertFrom-Json if ($value.SourceCommit -ne $env:REFERENCE_COMMIT -or $value.CaseCount -ne 25 -or $value.Cases.Count -ne 25 -or $value.Architecture -ne '${{ matrix.arch }}' -or $value.InvalidShaders -ne 0 -or $value.ShaderModel -ne 'ps_2_0' -or $value.ShaderRenderMode -ne 'SoftwareOnly') { throw 'Incomplete or wrong-identity reference.' } if ($value.CaptureMode -ne '${{ matrix.mode }}' -or $value.QualifiedShaderCases -ne ${{ matrix.qualified }} -or $value.UnmatchedShaderCases -ne ${{ matrix.unmatched }}) { throw 'Source capability control was confused with shader qualification.' } + $samplerReceipt = Join-Path $evidence 'image-samplers.json' + if ((Get-Item -LiteralPath $samplerReceipt).Length -gt 2MB) { throw 'Original sampler receipt exceeds its bound.' } + $samplers = Get-Content -LiteralPath $samplerReceipt -Raw | ConvertFrom-Json + $qualifiedSamplers = if ('${{ matrix.mode }}' -eq 'shader-pixels') { 24 } else { 0 } + if ($samplers.SourceCommit -ne $env:REFERENCE_COMMIT -or $samplers.Architecture -ne '${{ matrix.arch }}' -or $samplers.CaseCount -ne 24 -or $samplers.Cases.Count -ne 24 -or $samplers.Replays -ne 72 -or $samplers.IndependentColorCases -ne 18 -or $samplers.EquivalentViewboxPairs -ne 5 -or $samplers.Failures.Count -ne 0 -or $samplers.CaptureMode -ne '${{ matrix.mode }}' -or $samplers.QualifiedShaderCases -ne $qualifiedSamplers) { throw 'Incomplete or wrong-identity ImageBrush reference.' } Get-FileHash -Algorithm SHA256 -LiteralPath $receipt | Format-List } finally { if (!$child.HasExited) { $child.Kill($true); $child.WaitForExit(5000) | Out-Null } diff --git a/eng/WpfShaderEffectReference/ImageSamplers.cs b/eng/WpfShaderEffectReference/ImageSamplers.cs new file mode 100644 index 000000000..8e7b82fdd --- /dev/null +++ b/eng/WpfShaderEffectReference/ImageSamplers.cs @@ -0,0 +1,363 @@ +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text.Json; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Effects; +using System.Windows.Media.Imaging; +using System.Windows.Threading; + +internal static partial class Program +{ + private static void CaptureImageSamplers(string directory, string commit, bool unavailable, Stopwatch timer) + { + double startedMilliseconds = timer.Elapsed.TotalMilliseconds; + var observations = new List(); + var originals = new Dictionary(StringComparer.Ordinal); + var failures = new List(); + int independentColors = 0, equivalentPairs = 0; + foreach (SamplerCase input in SamplerCases()) + { + if (timer.Elapsed > TimeSpan.FromSeconds(60)) throw new TimeoutException("Original ImageBrush reference exceeded 60 seconds."); + var source = CreateSamplerBitmap(input, out byte[] sourcePixels); + Rect viewbox = input.AbsoluteViewbox + ? new(input.Viewbox.X * source.Width, input.Viewbox.Y * source.Height, + input.Viewbox.Width * source.Width, input.Viewbox.Height * source.Height) + : input.Viewbox; + var description = new + { + input, PixelWidth = source.PixelWidth, PixelHeight = source.PixelHeight, + source.DpiX, source.DpiY, SourceDipWidth = source.Width, SourceDipHeight = source.Height, + ActualViewbox = viewbox, ViewboxUnits = input.AbsoluteViewbox ? "Absolute" : "RelativeToBoundingBox", + SourcePixelSha256 = Convert.ToHexString(SHA256.HashData(sourcePixels)), + SourcePixelFormat = "Pbgra32", ShaderWords = SamplerWords, + SamplerRegister = 0, ShaderRenderMode = "SoftwareOnly", AlignmentX = "Center", AlignmentY = "Center", + BitmapScalingMode = "NearestNeighbor", ParentBitmapScalingMode = input.ParentNearest ? "NearestNeighbor" : "Unspecified", + VisualFieldScalingMode = input.VisualNearest ? "NearestNeighbor" : "Unspecified", + ParentVisualFieldScalingMode = input.ParentVisualNearest ? "NearestNeighbor" : "Unspecified", + EffectInputBounds = input.Bounds, + Meaning = "Original source bitmap metadata and complete ImageBrush before shader rendering." + }; + using (var file = new FileStream(Path.Combine(directory, input.Name + ".input.json"), FileMode.CreateNew)) + JsonSerializer.Serialize(file, description, new JsonSerializerOptions { WriteIndented = true }); + SaveSamplerBitmap(directory, input.Name + ".source", source, sourcePixels); + ContainerVisual visual = CreateSamplerVisual(input, source, viewbox); + byte[]? first = null; + for (int replay = 0; replay < 3; ++replay) + { + if (timer.Elapsed > TimeSpan.FromSeconds(60)) throw new TimeoutException("Original ImageBrush replay exceeded 60 seconds."); + var bitmap = new RenderTargetBitmap(input.Size, input.Size, 96, 96, PixelFormats.Pbgra32); + if (replay == 2) + { + var independent = CreateSamplerBitmap(input, out _); + visual = CreateSamplerVisual(input, independent, viewbox); + } + if (input.ResetVisualOptions && replay != 0) + { + var receivingVisual = (SamplerDrawingVisual)((ContainerVisual)visual.Children[1]).Children[0]; + receivingVisual.ResetScalingMode(); + if (receivingVisual.EmittedScalingMode != BitmapScalingMode.Unspecified) + throw new InvalidOperationException("Original receiving visual did not reset its actual options."); + } + bitmap.Render(visual); + Dispatcher.CurrentDispatcher.Invoke(static () => { }, DispatcherPriority.ApplicationIdle); + if (Volatile.Read(ref invalidShaders) != 0) throw new InvalidOperationException($"Original WPF rejected {input.Name}."); + var pixels = new byte[input.Size * input.Size * 4]; + bitmap.CopyPixels(pixels, input.Size * 4, 0); + if (replay == 0) SaveSamplerBitmap(directory, input.Name, bitmap, pixels); + // Retain the entire original inventory after a pixel mismatch, + // but never publish a successful receipt for those observations. + try { AssertSamplerPixels(input, pixels, unavailable); } + catch (InvalidOperationException error) { failures.Add($"Replay {replay}: {error.Message}"); } + if (first != null && !first.AsSpan().SequenceEqual(pixels)) + failures.Add($"{input.Name}: cold/warm/independent sampler pixels changed."); + first ??= pixels; + } + if (input.NativeVariant >= 0 || input.ExactViewboxMapping) ++independentColors; + if (input.EquivalentTo != null) + { + if (!originals.TryGetValue(input.EquivalentTo, out var prior) || !prior.AsSpan().SequenceEqual(first)) + failures.Add($"{input.Name}: equivalent original absolute/relative viewboxes differ."); + ++equivalentPairs; + } + originals.Add(input.Name, first!); + observations.Add(new + { + input.Name, Input = description, Replays = 3, Pixels = first, + PixelSha256 = Convert.ToHexString(SHA256.HashData(first!)), + IndependentColorOracle = input.NativeVariant >= 0 || input.ExactViewboxMapping, + input.EquivalentTo + }); + } + if (observations.Count != 24 || independentColors != 18 || equivalentPairs != 5) + throw new InvalidOperationException("Original ImageBrush inventory is incomplete."); + var receipt = new + { + Schema = 1, SourceCommit = commit, Architecture = RuntimeInformation.ProcessArchitecture.ToString(), + PresentationIdentity = typeof(ShaderEffect).Assembly.FullName, + PresentationCore = FileIdentity(typeof(ShaderEffect).Assembly.Location), + Producer = FileIdentity(Assembly.GetExecutingAssembly().Location), + CaseCount = observations.Count, Cases = observations, Replays = 72, + CaptureMode = unavailable ? "unsupported-software-control" : "shader-pixels", + QualifiedShaderCases = unavailable || failures.Count != 0 ? 0 : observations.Count, + IndependentColorCases = independentColors, EquivalentViewboxPairs = equivalentPairs, + Failures = failures, + ElapsedMilliseconds = timer.Elapsed.TotalMilliseconds - startedMilliseconds, + Qualification = unavailable + ? "Original ARM64 unavailable-software control; no sampler shader pixels qualified." + : "Original Microsoft WPF ImageBrush observations. Eighteen independent color controls and five complete-pixel equivalence pairs; failures disqualify the entire capture. Not native/source-host/package parity." + }; + using (var output = new FileStream(Path.Combine(directory, failures.Count == 0 ? "image-samplers.json" : "image-samplers.failed.json"), FileMode.CreateNew)) + JsonSerializer.Serialize(output, receipt, new JsonSerializerOptions { WriteIndented = true }); + if (failures.Count != 0) throw new InvalidOperationException(string.Join(Environment.NewLine, failures)); + Console.WriteLine($"Original ImageBrush: 24 cases / 72 replays / 5 viewbox pairs; {(unavailable ? 0 : 24)} source shader cases qualified."); + } + + private static readonly uint[] SamplerWords = [0xFFFF0200, 0x0200001F, 0x80000000, 0xB0030000, + 0x0200001F, 0x90000000, 0xA00F0800, 0x03000042, 0x800F0000, 0xB0E40000, 0xA0E40800, + 0x02000001, 0x800F0800, 0x80E40000, 0xFFFF]; + + private sealed record SamplerCase(string Name) + { + public int Size { get; init; } = 64; + public int Width { get; init; } = 2; + public int Height { get; init; } = 1; + public double DpiX { get; init; } = 144; + public double DpiY { get; init; } = 192; + public double Opacity { get; init; } = .5; + public Rect Bounds { get; init; } = new(8, 10, 32, 24); + public Rect Clip { get; init; } = new(16, 12, 16, 16); + public Rect Viewbox { get; init; } = new(0, 0, 1, 1); + public Rect Viewport { get; init; } = new(0, 0, 1, 1); + public bool AbsoluteViewbox { get; init; } + public Stretch Stretch { get; init; } = Stretch.Fill; + public TileMode Tile { get; init; } = TileMode.None; + public SamplingMode Sampling { get; init; } = SamplingMode.NearestNeighbor; + public double TranslationX { get; init; } + public bool Blue { get; init; } + public int NativeVariant { get; init; } = -1; + public bool ExactViewboxMapping { get; init; } + public bool ParentNearest { get; init; } + public bool VisualNearest { get; init; } + public bool ParentVisualNearest { get; init; } + public bool ResetVisualOptions { get; init; } + public string? EquivalentTo { get; init; } + } + + private static IEnumerable SamplerCases() + { + for (int variant = 0; variant < 4; ++variant) + yield return new($"sampler-native-{variant}") + { + NativeVariant = variant, Blue = variant == 3, Opacity = variant == 3 ? 1 : .5, + Viewport = new(0, 0, variant == 0 ? 1 : .5, 1), Tile = variant == 0 ? TileMode.None : TileMode.Tile, + TranslationX = variant == 2 ? 8 : 0, + Sampling = (variant & 1) == 0 ? SamplingMode.NearestNeighbor : SamplingMode.Bilinear + }; + for (int variant = 0; variant < 2; ++variant) + yield return new($"sampler-parent-nearest-{variant}") + { + NativeVariant = variant, ParentNearest = true, + Viewport = new(0, 0, variant == 0 ? 1 : .5, 1), Tile = variant == 0 ? TileMode.None : TileMode.Tile, + Sampling = variant == 0 ? SamplingMode.NearestNeighbor : SamplingMode.Bilinear + }; + foreach (bool fractional in new[] { false, true }) + foreach (SamplingMode sampling in new[] { SamplingMode.NearestNeighbor, SamplingMode.Bilinear }) + { + string name = $"sampler-crop-{(fractional ? "fractional" : "integral")}-{sampling}"; + var input = new SamplerCase(name + "-absolute") + { + Size = 128, Width = 400, Height = 200, DpiX = fractional ? 123.456789012345 : 192, + DpiY = fractional ? 183.456789012345 : 384, Opacity = .25, + Bounds = new(8, 10, 100, 100), Clip = new(8, 10, 100, 100), + Viewbox = new(.25, .2, .5, .4), AbsoluteViewbox = true, Stretch = Stretch.None, + Sampling = sampling, ExactViewboxMapping = !fractional + }; + yield return input; + yield return input with { Name = name + "-relative", AbsoluteViewbox = false, EquivalentTo = input.Name }; + } + var mirrored = new SamplerCase("sampler-flip-translated-absolute") + { + Size = 128, Width = 400, Height = 200, DpiX = 192, DpiY = 384, Opacity = .25, + Bounds = new(8, 10, 100, 100), Clip = new(8, 10, 100, 100), + Viewbox = new(.25, .2, .5, .4), Viewport = new(0, 0, .5, 1), AbsoluteViewbox = true, + Stretch = Stretch.None, Tile = TileMode.FlipX, TranslationX = 8 + }; + yield return mirrored; + yield return mirrored with { Name = "sampler-flip-translated-relative", AbsoluteViewbox = false, EquivalentTo = mirrored.Name }; + // Preserve the original sixteen inputs above. These eight additional + // cases set the actual protected visual state serialized into MIL; + // attached DPs on bare Visuals are not equivalent emitted options. + for (int variant = 0; variant < 4; ++variant) + yield return new($"sampler-visual-options-{variant}") + { + NativeVariant = variant, VisualNearest = true, Blue = variant == 3, Opacity = variant == 3 ? 1 : .5, + Viewport = new(0, 0, variant == 0 ? 1 : .5, 1), Tile = variant == 0 ? TileMode.None : TileMode.Tile, + TranslationX = variant == 2 ? 8 : 0, + Sampling = (variant & 1) == 0 ? SamplingMode.NearestNeighbor : SamplingMode.Bilinear + }; + for (int variant = 0; variant < 2; ++variant) + yield return new($"sampler-parent-visual-options-{variant}") + { + NativeVariant = variant, ParentVisualNearest = true, + Viewport = new(0, 0, variant == 0 ? 1 : .5, 1), Tile = variant == 0 ? TileMode.None : TileMode.Tile, + Sampling = variant == 0 ? SamplingMode.NearestNeighbor : SamplingMode.Bilinear + }; + for (int variant = 0; variant < 2; ++variant) + yield return new($"sampler-reset-to-inherited-options-{variant}") + { + NativeVariant = variant, ParentVisualNearest = true, VisualNearest = true, ResetVisualOptions = true, + Viewport = new(0, 0, variant == 0 ? 1 : .5, 1), Tile = variant == 0 ? TileMode.None : TileMode.Tile, + Sampling = variant == 0 ? SamplingMode.NearestNeighbor : SamplingMode.Bilinear + }; + } + + private static BitmapSource CreateSamplerBitmap(SamplerCase input, out byte[] pixels) + { + pixels = new byte[input.Width * input.Height * 4]; + for (int y = 0; y < input.Height; ++y) + for (int x = 0; x < input.Width; ++x) + { + int at = (y * input.Width + x) * 4; + pixels[at + (x >= input.Width / 2 ? 1 : input.Blue ? 0 : 2)] = 255; + pixels[at + 3] = 255; + } + return BitmapSource.Create(input.Width, input.Height, input.DpiX, input.DpiY, PixelFormats.Pbgra32, + null, pixels, input.Width * 4); + } + + private static ContainerVisual CreateSamplerVisual(SamplerCase input, BitmapSource bitmap, Rect viewbox) + { + var brush = new ImageBrush(bitmap) + { + Opacity = input.Opacity, Viewbox = viewbox, + ViewboxUnits = input.AbsoluteViewbox ? BrushMappingMode.Absolute : BrushMappingMode.RelativeToBoundingBox, + Viewport = input.Viewport, ViewportUnits = BrushMappingMode.RelativeToBoundingBox, + Stretch = input.Stretch, TileMode = input.Tile, AlignmentX = AlignmentX.Center, AlignmentY = AlignmentY.Center, + Transform = new TranslateTransform(input.TranslationX, 0) + }; + RenderOptions.SetBitmapScalingMode(brush, BitmapScalingMode.NearestNeighbor); + var root = new ContainerVisual(); + var background = new DrawingVisual(); + using (var drawing = background.RenderOpen()) drawing.DrawRectangle(Brushes.Black, null, new(0, 0, input.Size, input.Size)); + root.Children.Add(background); + var clip = new SamplerContainerVisual(input.ParentVisualNearest) { Clip = new RectangleGeometry(input.Clip) }; + if (input.ParentNearest) RenderOptions.SetBitmapScalingMode(clip, BitmapScalingMode.NearestNeighbor); + var source = new SamplerDrawingVisual(input.VisualNearest) { Effect = new SamplerEffect(brush, input.Sampling) }; + RenderOptions.SetBitmapScalingMode(source, BitmapScalingMode.NearestNeighbor); + if (source.EmittedScalingMode != (input.VisualNearest ? BitmapScalingMode.NearestNeighbor : BitmapScalingMode.Unspecified) || + clip.EmittedScalingMode != (input.ParentVisualNearest ? BitmapScalingMode.NearestNeighbor : BitmapScalingMode.Unspecified)) + throw new InvalidOperationException($"{input.Name}: actual visual state does not match the emitted-option contract."); + using (var drawing = source.RenderOpen()) drawing.DrawRectangle(Brushes.White, null, input.Bounds); + clip.Children.Add(source); + root.Children.Add(clip); + return root; + } + + private static void AssertSamplerPixels(SamplerCase input, byte[] pixels, bool unavailable) + { + int changed = 0; + for (int y = 0; y < input.Size; ++y) + for (int x = 0; x < input.Size; ++x) + { + bool inside = input.Clip.Contains(new Point(x + .5, y + .5)) && input.Bounds.Contains(new Point(x + .5, y + .5)); + int at = (y * input.Size + x) * 4; + if (pixels[at + 3] != 255) throw new InvalidOperationException($"{input.Name}: final opaque alpha changed at {x},{y}."); + for (int channel = 0; channel < 3; ++channel) + { + byte actual = pixels[at + channel]; + if (inside && actual != 0 && actual != 255) ++changed; + byte expected = 0; + bool exact = unavailable || !inside || input.NativeVariant >= 0 || input.ExactViewboxMapping; + // On native ARM64 the unavailable software ImageBrush shader + // contributes no color, unlike the separate implicit-input + // controls that retain their original white input. Neither + // behavior qualifies shader execution on that architecture. + if (!unavailable && inside && input.NativeVariant >= 0 && (input.VisualNearest || input.ParentVisualNearest)) + { + int variant = input.NativeVariant; + int stripe = ((x - 8 + 32 - (variant == 2 ? 8 : 0)) / (variant == 0 ? 16 : 8)) & 1; + int color = stripe == 1 ? 1 : input.Blue ? 0 : 2; + if (channel == color) expected = input.Blue ? (byte)255 : (byte)128; + } + else if (!unavailable && inside && input.NativeVariant >= 0) + { + int variant = input.NativeVariant; + // Independent two-texel linear realization at pixel centers. + // The attached nearest DP on a bare Visual does not change + // its actual visual field, so these cases emit Unspecified. + // Actual visual-option cases retain their nearest oracle. + double coordinate = (x - 8 + .5 - input.TranslationX) / (variant == 0 ? 16 : 8) - .5; + int left = (int)Math.Floor(coordinate); + double fraction = coordinate - left; + int first = variant == 0 ? Math.Clamp(left, 0, 1) : ((left % 2) + 2) % 2; + int second = variant == 0 ? Math.Clamp(left + 1, 0, 1) : (((left + 1) % 2) + 2) % 2; + double green = (first == 1 ? 1 - fraction : 0) + (second == 1 ? fraction : 0); + double component = channel == 1 ? green : channel == (input.Blue ? 0 : 2) ? 1 - green : 0; + expected = checked((byte)Math.Round(component * 255 * input.Opacity, MidpointRounding.ToEven)); + } + // Stretch=None maps the selected 100x20 viewbox to (0,40), + // subtracting its (50,10) origin. TileMode.None preserves the + // full source 200x50 extent: output y=10+(40-10)..+50. + else if (!unavailable && inside && input.ExactViewboxMapping && y >= 40 && y < 90) + expected = channel == (x < 58 ? 2 : 1) ? (byte)64 : (byte)0; + if (exact && actual != expected) + throw new InvalidOperationException($"{input.Name}: ({x},{y}) BGRA[{channel}]={actual}, expected {expected}."); + } + } + if (!unavailable && input.NativeVariant < 0 && changed == 0) + throw new InvalidOperationException($"{input.Name}: sampler capture is blank or unmodified, not an opacity-bearing shader result."); + } + + private static void SaveSamplerBitmap(string directory, string name, BitmapSource bitmap, byte[] pixels) + { + using (var raw = new FileStream(Path.Combine(directory, name + ".bgra"), FileMode.CreateNew)) raw.Write(pixels); + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(bitmap)); + using var image = new FileStream(Path.Combine(directory, name + ".png"), FileMode.CreateNew); + encoder.Save(image); + } + + private sealed class SamplerDrawingVisual : DrawingVisual + { + public SamplerDrawingVisual(bool nearest) + { if (nearest) VisualBitmapScalingMode = BitmapScalingMode.NearestNeighbor; } + public BitmapScalingMode EmittedScalingMode => VisualBitmapScalingMode; + public void ResetScalingMode() => VisualBitmapScalingMode = BitmapScalingMode.Unspecified; + } + + private sealed class SamplerContainerVisual : ContainerVisual + { + public SamplerContainerVisual(bool nearest) + { if (nearest) VisualBitmapScalingMode = BitmapScalingMode.NearestNeighbor; } + public BitmapScalingMode EmittedScalingMode => VisualBitmapScalingMode; + } + + private sealed class SamplerEffect : ShaderEffect + { + private static readonly DependencyProperty NearestInput = RegisterPixelShaderSamplerProperty( + "NearestInput", typeof(SamplerEffect), 0, SamplingMode.NearestNeighbor); + private static readonly DependencyProperty LinearInput = RegisterPixelShaderSamplerProperty( + "LinearInput", typeof(SamplerEffect), 0, SamplingMode.Bilinear); + private readonly ImageBrush brush; + private readonly SamplingMode sampling; + + public SamplerEffect(ImageBrush brush, SamplingMode sampling) + { + this.brush = brush; this.sampling = sampling; + var bytes = new byte[SamplerWords.Length * 4]; + for (int i = 0; i < SamplerWords.Length; ++i) BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(i * 4, 4), SamplerWords[i]); + var shader = new PixelShader { ShaderRenderMode = ShaderRenderMode.SoftwareOnly }; + using (var stream = new MemoryStream(bytes, writable: false)) shader.SetStreamSource(stream); + PixelShader = shader; + var property = sampling == SamplingMode.NearestNeighbor ? NearestInput : LinearInput; + SetValue(property, brush); UpdateShaderValue(property); + } + + protected override Freezable CreateInstanceCore() => new SamplerEffect(brush, sampling); + } +} diff --git a/eng/WpfShaderEffectReference/Program.cs b/eng/WpfShaderEffectReference/Program.cs index 1a4799731..5d58d897c 100644 --- a/eng/WpfShaderEffectReference/Program.cs +++ b/eng/WpfShaderEffectReference/Program.cs @@ -12,7 +12,7 @@ using System.Windows.Media.Media3D; using System.Windows.Threading; -internal static class Program +internal static partial class Program { private const int ExpectedCases = 25; private static int invalidShaders; @@ -113,6 +113,7 @@ private static void Main(string[] args) .Where(module => string.Equals(module.ModuleName, "wpfgfx_cor3.dll", StringComparison.OrdinalIgnoreCase)) .Select(module => FileIdentity(module.FileName)).ToArray(); if (modules.Length != 1) throw new InvalidOperationException("Original native WPF renderer identity is missing or ambiguous."); + CaptureImageSamplers(directory, args[2], unavailableControl, timer); var receipt = new { Schema = 1, SourceCommit = args[2], CaseCount = observations.Count, Cases = observations, diff --git a/eng/WpfShaderEffectReference/README.md b/eng/WpfShaderEffectReference/README.md index 93f0e07e2..dc3bfb041 100644 --- a/eng/WpfShaderEffectReference/README.md +++ b/eng/WpfShaderEffectReference/README.md @@ -58,3 +58,132 @@ Windows-targeted project compiled from the existing cached reference pack in 1.42 seconds, with zero warnings/errors and no ProGPU renderer dependency. Windows execution is delegated to the hosted reference workflow; this local compile does not constitute a reference capture or pixel qualification. + +## Original ImageBrush sampler frames + +An additive, separate `image-samplers.json` receipt keeps every original arithmetic +case unchanged and captures 24 original ImageBrush cases, each cold, warm and with +an independently recreated visual and bitmap. Eight inputs set actual protected +visual options, including two same-visual resets to inherited options after the +first capture. Four of those inputs match the original ProGPU-owned sampler +fixture at `b044a8cee25ea32be4842253e013381882c3d023`, +`src/ProGPU.Native/tests/progpu_native_shader_sampler_pixel_fixture.hpp`. +Six earlier inputs deliberately retain attached properties on bare visuals; +those do not set the actual visual options and keep their linear expectations. +Source bitmap DPI (144,192), brush opacity, addressing, translation, independent +shader sampling and final source clip all remain observable. Every output byte +in these fourteen small inputs has an independent nearest or linear expectation; +the source bitmap and complete original brush input are saved too. + +The other ten inputs use a 400x200 bitmap with independent source DPI axes, +absolute/relative equivalent viewboxes, Stretch=None in a 100x100 effect input, +nearest/bilinear shader sampling, and translated FlipX tiles. The integral +(192,384)-DPI viewbox retains a 100x20-DIP mapping within a full 200x50-DIP +source image. Its four plain cases must preserve all 50 source rows after mapping, +with quarter-opacity red/green pixels rather than clipping to the viewbox. +Fractional DPI (123.456789012345,183.456789012345) and mirrored cases retain +every original pixel without inventing an idealized float-to-DIP oracle. Five +absolute/relative pairs must match completely, preserve final clipping/opaque +alpha, and have actual opacity-bearing output rather than blank or unchanged +white. Those pairs are not labeled independent color-oracle cases. + +Inputs use public original `BitmapSource.Width/Height` for absolute viewboxes, +not a replacement double DPI formula. PixelShader sampler registration and actual +ImageBrush realization remain Microsoft WPF. This adds no production renderer, +native shader, CPU fallback or source admission. ARM64 executes all inputs as +the existing explicit unavailable-software control, reporting zero qualified +sampler shader cases. All previous deadlines, color assertions and native/package +gates remain; a reference mismatch fails rather than removing its case. + +After the implementation commit, the small original-only Windows-targeted project +compiled against cached reference assemblies in 1.51 seconds with zero warnings +or errors. This host used installed SDK 11 preview with target `net10.0-windows`; +it installed no SDK and loaded no WPF renderer or ProGPU native runtime. Actual +Windows source capture remains the hosted workflow's responsibility. The new +sampler phase shares the original process-wide 60-second stopwatch rather than +resetting the capture deadline. + +The first hosted original x64 run `37010052606` (job `110847365474`) passed the +25 arithmetic inputs but contradicted the existing native fixture's hard stripe +expectation. `sampler-native-0` returned a linear red/green ramp at x=16..31: +BGRA `(0,4,124,255)` through `(0,124,4,255)`, despite nearest options on the effect +visual and its ImageBrush. These failed original PNG/raw/input artifacts are +retained; no native parity claim or passing sampler receipt follows from them. + +Two additive parent-Nearest controls isolate incoming render-state inheritance +from options on the effect visual itself, bringing the inventory to 16 inputs, +48 replays and 10 independent color controls. Pixel failures now accumulate so +all original inputs and equivalence pairs are observed in one bounded run. Any +failure writes only `image-samplers.failed.json`, reports zero qualified cases +and fails the workflow after capture. None of the pixel assertions, original +cases or capability checks is weakened. The producer behavior being investigated +is documented in the original WPF `ShaderEffect.cpp` secondary-input capture: +its fresh capture context inherits the incoming render state. This observation +does not establish the effective render-option ordering by itself. + +The complete native ARM64 original capture `37010911430`, job `110850135785`, +established a distinct unavailable-software outcome for all 16 ImageBrush inputs: +their shader contributes no color, leaving every BGRA pixel `(0,0,0,255)` over +the original black background. The old 25 implicit-input controls still produce +their required clipped white input. Their checks stay unchanged; the new sampler +negative control checks every byte against the observed empty contribution and +continues to report zero qualified shader cases. All input/PNG/raw artifacts and +the failed initial white-input assumption remain recorded. This architecture's +negative outcome must never become a product pixel expectation or replace the +positive x64 or native ARM64 GPU gates. + +The completed x64 diagnostic receipt from the same run has SHA256 +`17f013af6e4936e2f36bbc753635712a5bbc088e41d9cfcf8ed43bdc98d6a356`. +Both parent-Nearest cases are byte-identical to their effect-visual counterparts; +the evidence therefore does **not** support an incoming-options-only product fix. +The original software pixel expectation now uses an independent two-texel linear +interpolation formula, pixel centers, clamp/repeat addressing and exact UNORM +rounding. It does not replace original inputs, embed captured pixel arrays or +introduce a tolerance. Whether hardware capture has the same realization policy +remains a distinct requirement before changing native/GPU defaults. + +The integral source viewbox also selects a mapping, not an image crop for +TileMode.None. Its Stretch=None mapping places the 100x20 viewbox at (0,40) in +the 100x100 input, subtracting source origin (50,10). The complete 200x50-DIP +image thus occupies input y=30..80, or final y=40..90 after the visual origin; +every observed colored row is 40..89, not the initially predicted50..69. +The independent expected frame now preserves that full source overflow and +checks every pixel. All original input cases, failed receipts and exact +absolute/relative comparisons are retained. These observations expose missing +source/native qualification rather than establish product parity. + +## Actual visual options versus attached properties + +Original `RenderOptions.cs` registers the BitmapScalingMode attached property +without a changed callback for bare visuals. Original `UIElement.cs` overrides +its metadata and propagates into `Visual.VisualBitmapScalingMode`; `Visual.cs` +serializes that separate protected state into MIL. DrawingGroup has its own +property serialization. Consequently, the earlier bare DrawingVisual/ContainerVisual +inputs never emitted nearest options. Their linear output does not imply that +the native renderer must ignore valid nearest options or change its default. + +The additional eight controls use small derived DrawingVisual/ContainerVisual +types to set and read the original protected property directly, without private +reflection or foreign implementation. Four set nearest on the receiving effect +visual; two inherit from a nearest parent; two capture that same nearest parent +and receiving visual, then reset the already-captured receiving visual to +Unspecified before its next capture. Independent visual replay uses the final +inherited state too. The nearest oracle remains the original strict stripe +formula. The first sixteen inputs, their actual unspecified visual fields, +independent linear/viewbox formulas, and historical failures remain intact. +This gives 24 sampler cases, 72 replays, 18 independent color cases and five +complete-pixel equivalence pairs, still within the shared 60-second deadline. + +The exact source `5e13e5863053f9e5836fda323fdd52be6fe8a70b` completed hosted +workflow `37013264907` successfully on both Windows architectures. The x64 job +`110857864291` qualified all 24 sampler inputs and 72 replays, with all eighteen +independent color controls and five complete-pixel pairs passing. Its sampler +receipt SHA256 is +`d90dbd21d70869d1b42da5145e89e976c365d0134d015d38335f7efc151821b0`. +The ARM64 job `110857864140` passed the unavailable-software controls with zero +qualified sampler shaders; its receipt SHA256 is +`14bf8daed4e49f07f8734976287a22c2184d4a05026d1424265ba297c559517c`. +Both receipts retain the original PresentationCore identity, actual visual-field +state, all pixels and independent replays. The unchanged 25 arithmetic inputs +also passed their architecture-specific controls. This establishes the original +reference, not a native renderer, source-host or package qualification.