fix #130: doorway-slice scissor cut the aperture's top/right pixel row
The user's "thin strip of background color along the TOP outer edge of a
doorway, looking out from inside" is the landscape-slice scissor box, not
the W=0 clip port.
Mechanism (pinned headlessly, Issue130DoorwayStripTests, 147 eye/gaze
combos at the real Holtburg A9B4 0x0170 exit door):
- BeginDoorwayScissor converted the slice NDC AABB to pixels as
Floor(origin) + Ceiling(size). The far edge floor(min)+ceil(max-min)
lands up to ONE PIXEL SHORT of the true top/right edge at unlucky
fractional alignments (captured: top edge y=0.7938 @1080p -> row 968
cut; right edge column 1296 @1920 cut).
- The scissor brackets the ENTIRE landscape slice (sky, terrain, outdoor
statics, weather). The exit-portal SEAL stamps the full raw aperture at
true depth and the shell wall ends at the aperture edge, so the cut row
never receives any color write -> clear color, flickering with eye
movement as the fractional alignment shifts.
- This violated AD-17's own invariant (over-inclusion is safe,
UNDER-inclusion is the bug class). No register change: the fix restores
the row's documented doctrine.
Lead 1 (987313a W=0 clip port regression) REFUTED by the same harness:
the CPU polygon pipeline (ProjectToClip -> ClipToRegion merges ->
ClipPlaneSet planes) is sub-pixel exact against the raw aperture
projection (worst 0.54 px, 0.00 px aligned). For an all-in-front doorway
polygon the port is bit-identical to the old 1e-4 path by construction.
The EyeInsidePortalOpening rescue stays deleted.
Fix: conservative outer bound floor(min)/ceil(max) extracted to
NdcScissorRect.ToPixels (GL-free; containment property proven in the
header comment); BeginDoorwayScissor delegates.
Pins:
- NdcScissorRectTests: center-inside containment across 251 fractional
alignments x 2 framebuffer sizes + both captured regression cases.
- Issue130DoorwayStripTests: production flood + assembler at the real
exit door; asserts the scissor never cuts a plane-admitted fragment
(worstScissorGap 0.00 px post-fix, was 10.8 px capped) and the CPU
pipeline stays sub-pixel exact (canary 1.2 px).
Suites: App 252+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user visual gate at a cottage doorway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0cb97aa594
commit
6c4b6d64d9
5 changed files with 494 additions and 40 deletions
|
|
@ -9954,26 +9954,18 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
// Phase W Stage 4: set a glScissor to an NDC AABB (the doorway / OutsideView region) in
|
||||
// framebuffer pixels and enable the scissor test; returns true iff applied (the caller then
|
||||
// disables EnableCap.ScissorTest after its draw/clear). Mirrors the terrain Scissor-mode
|
||||
// NDC→pixel conversion (one source for the box math). Used to confine the sky/weather particle
|
||||
// passes (particle.vert has no gl_ClipDistance) and the conditional doorway depth-only Z-clear
|
||||
// to the doorway opening. Returns false (no scissor) when not applied (outdoor / no window).
|
||||
// disables EnableCap.ScissorTest after its draw/clear). Used to bracket the landscape slice
|
||||
// (sky, terrain, statics, weather — particle.vert has no gl_ClipDistance). Returns false
|
||||
// (no scissor) when not applied (outdoor / no window). The box is the CONSERVATIVE outer
|
||||
// bound (NdcScissorRect): the previous Floor(origin)+Ceiling(size) form cut up to one pixel
|
||||
// off the TOP/RIGHT edges at unlucky alignments — the #130 doorway top-edge background strip.
|
||||
private bool BeginDoorwayScissor(bool apply, System.Numerics.Vector4 ndcAabb)
|
||||
{
|
||||
if (!apply || _window is null) return false;
|
||||
var fb = _window.FramebufferSize;
|
||||
// NDC [-1,1] → window pixels. Clamp so a doorway opening that extends past a screen edge
|
||||
// still yields a valid box (same clamp the terrain Scissor path uses).
|
||||
float nx0 = System.Math.Clamp(ndcAabb.X, -1f, 1f);
|
||||
float ny0 = System.Math.Clamp(ndcAabb.Y, -1f, 1f);
|
||||
float nx1 = System.Math.Clamp(ndcAabb.Z, -1f, 1f);
|
||||
float ny1 = System.Math.Clamp(ndcAabb.W, -1f, 1f);
|
||||
int px = (int)System.MathF.Floor((nx0 * 0.5f + 0.5f) * fb.X);
|
||||
int py = (int)System.MathF.Floor((ny0 * 0.5f + 0.5f) * fb.Y);
|
||||
int pw = (int)System.MathF.Ceiling((nx1 - nx0) * 0.5f * fb.X);
|
||||
int ph = (int)System.MathF.Ceiling((ny1 - ny0) * 0.5f * fb.Y);
|
||||
var box = NdcScissorRect.ToPixels(ndcAabb, fb.X, fb.Y);
|
||||
_gl!.Enable(EnableCap.ScissorTest);
|
||||
_gl.Scissor(px, py, (uint)System.Math.Max(1, pw), (uint)System.Math.Max(1, ph));
|
||||
_gl.Scissor(box.X, box.Y, (uint)box.Width, (uint)box.Height);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
45
src/AcDream.App/Rendering/NdcScissorRect.cs
Normal file
45
src/AcDream.App/Rendering/NdcScissorRect.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// NdcScissorRect.cs
|
||||
//
|
||||
// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound).
|
||||
// The scissor that brackets a landscape/doorway slice is a fallback BOUND on
|
||||
// the slice's view region (AD-17 in the divergence register): it must CONTAIN
|
||||
// every fragment the per-fragment plane clip would keep. Under-inclusion is
|
||||
// the bug class — the #130 doorway top-edge background strip was this box
|
||||
// computed as Floor(origin) + Ceiling(size), whose far edge
|
||||
// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge
|
||||
// at unlucky fractional alignments, scissoring away the aperture's top/right
|
||||
// pixel row for the whole slice (sky, terrain, statics, weather) while the
|
||||
// seal still stamps it — a strip of clear color no later pass can fill.
|
||||
//
|
||||
// Correct outer bound: floor both mins, ceil both maxes, width = difference.
|
||||
// A fragment at pixel (i,j) rasterizes iff its CENTER (i+0.5, j+0.5) lies in
|
||||
// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒
|
||||
// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So
|
||||
// [floor(X0), ceil(X1)) admits every center-inside pixel, over-including by
|
||||
// at most one pixel per edge — safe per AD-17's doctrine (the wall shell /
|
||||
// plane clip repaints or kills the surplus).
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public static class NdcScissorRect
|
||||
{
|
||||
/// <summary>Convert an NDC AABB (minX, minY, maxX, maxY in [-1,1]) to a
|
||||
/// framebuffer-pixel scissor box that CONTAINS it. Inputs are clamped to
|
||||
/// the screen so a region extending past an edge still yields a valid box.
|
||||
/// Width/height are at least 1.</summary>
|
||||
public static (int X, int Y, int Width, int Height) ToPixels(
|
||||
Vector4 ndcAabb, int fbWidth, int fbHeight)
|
||||
{
|
||||
float nx0 = Math.Clamp(ndcAabb.X, -1f, 1f);
|
||||
float ny0 = Math.Clamp(ndcAabb.Y, -1f, 1f);
|
||||
float nx1 = Math.Clamp(ndcAabb.Z, -1f, 1f);
|
||||
float ny1 = Math.Clamp(ndcAabb.W, -1f, 1f);
|
||||
int px0 = (int)MathF.Floor((nx0 * 0.5f + 0.5f) * fbWidth);
|
||||
int py0 = (int)MathF.Floor((ny0 * 0.5f + 0.5f) * fbHeight);
|
||||
int px1 = (int)MathF.Ceiling((nx1 * 0.5f + 0.5f) * fbWidth);
|
||||
int py1 = (int)MathF.Ceiling((ny1 * 0.5f + 0.5f) * fbHeight);
|
||||
return (px0, py0, Math.Max(1, px1 - px0), Math.Max(1, py1 - py0));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue