fix(compositor): keep straight alpha in the paste-cache raster — the web widget's recording black box
The element-space raster builds on a TRANSPARENT base but BlitContentRaw's partial-alpha branch applied the opaque-dst source-over blend: color premultiplied by the sampled alpha, then alpha forced to 255. Pasting that raster saw a==255 and straight-copied darkened ink over the scene — a recording box that the raw-bitmap preview (correct alpha) never showed. Transparent margins and opaque content were unaffected, which is why every take looped on the page/CSS while the capture was transparent all along (15:51 dumps: alpha max 255, mean ~19, zero 57%). BlitContentRaw now takes transparentDst; the raster call passes true and writes straight color + straight alpha so the paste rows (BlendRowOpaque/Weighted) do the real source-over onto the opaque master. Master paths byte-identical. ONE integration test PasteCache_SemiTransparentLayer_RevealsBackdrop_NotOpaqueInk: 50%-blue over red reads (127,0,128) fixed vs (0,0,128) buggy — proven both ways (verified by stashing the fix: fails before, passes after). Clean build, 0 warnings; 22/23 compositor-class tests pass, the sole failure the documented pre-existing Composite_FullScene_MasterPixels pixel (1380,700). Alpha-compositing model: standard source-over with producer-cached surfaces, the OBS/libyuv paste model already cited in ai.md/MyMistakes (rawvideo recipe, row-blit BLEND_NONE / straight-alpha branches); full story in MyMistakes (RESOLVED entry). Per the good-dog rule: one integration test, memory updates (MyMistakes/ai.md/ HANDOFF) in the same commit.
This commit is contained in:
@@ -254,6 +254,55 @@ public class SceneCompositorTests
|
||||
Assert.Equal(out2.BgraPixels, out3.BgraPixels);
|
||||
}
|
||||
|
||||
/// <summary>THE page-transparency fix (the web widget's recording "black box"): a
|
||||
/// semi-transparent layer flowing through the paste-cache raster must reveal the
|
||||
/// backdrop through it (true source-over), never an opaque darkened block. The raster
|
||||
/// is built on a TRANSPARENT base, but the old partial-alpha branch applied the
|
||||
/// opaque-dst blend (premultiplied color + forced alpha 255), so every translucent
|
||||
/// widget pixel pasted as opaque ink — the box in recordings while the raw-bitmap
|
||||
/// preview stayed correct. A half-alpha blue widget over a red backdrop must read
|
||||
/// (127,0,128) here; the buggy raster pastes (0,0,128).</summary>
|
||||
[Fact]
|
||||
public void PasteCache_SemiTransparentLayer_RevealsBackdrop_NotOpaqueInk()
|
||||
{
|
||||
var red = Solid(128, 64, 255, 0, 0); // live backdrop
|
||||
var web = new byte[32 * 16 * 4]; // 2:1 widget canvas, half-alpha blue, uniform
|
||||
for (var i = 0; i < web.Length; i += 4)
|
||||
{
|
||||
web[i] = 255; web[i + 1] = 0; web[i + 2] = 0; web[i + 3] = 128;
|
||||
}
|
||||
var semitransparent = new VideoFrame(32, 16, web); // NOT IsOpaque
|
||||
|
||||
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
|
||||
var widget = new Source { Type = SourceType.WebSource, X = 8, Y = 8, Width = 16, Height = 16 };
|
||||
var scene = new Scene { Name = "Live" };
|
||||
scene.Elements.Add(background);
|
||||
scene.Elements.Add(widget);
|
||||
|
||||
VideoFrame? FrameFor(SceneElement e) => e switch
|
||||
{
|
||||
Source { IsBackground: true } => red,
|
||||
Source { Type: SourceType.WebSource } => semitransparent,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
var options = new CompositorOptions
|
||||
{
|
||||
SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 128, SourceRectHeight = 64,
|
||||
OutputWidth = 128, OutputHeight = 64,
|
||||
};
|
||||
|
||||
var compositor = new SceneCompositor();
|
||||
var build = compositor.Render(scene, FrameFor, null, options); // raster build + paste
|
||||
var hit = compositor.Render(scene, FrameFor, null, options); // paste-cache hit
|
||||
foreach (var output in new[] { build, hit })
|
||||
{
|
||||
AssertColor(output, 16, 16, 127, 0, 128); // backdrop shows through the 50% blue
|
||||
AssertColor(output, 4, 4, 255, 0, 0); // outside the element: untouched backdrop
|
||||
}
|
||||
Assert.Equal(build.BgraPixels, hit.BgraPixels);
|
||||
}
|
||||
|
||||
/// <summary>take-16 regression gate — GC churn convicted mechanically: a ring-style
|
||||
/// producer hands out a small set of rotating arrays with a NEW EPOCH every frame
|
||||
/// (exactly what the capture/camera rings do). The compositor must re-rasterize INTO
|
||||
|
||||
Reference in New Issue
Block a user