diff --git a/docs/ISSUES.md b/docs/ISSUES.md index a9c38a4c..2bf4cc74 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -57,6 +57,11 @@ What does NOT go here: multi-session host, shared immutable content, deterministic bot API, 1/5/10/30-session and two-hour simulated gates, connected portal/movement parity, resource ceilings, and graceful zero-debt teardown. +- **Active rendering campaign:** Campaign V, OpenGL → Vulkan, executes from + [`plans/2026-07-27-vulkan-campaign.md`](plans/2026-07-27-vulkan-campaign.md). + V0 pinned the RHI contract, V1 landed the GL backend, V2 migrated the shaders + and CPU batch data to texture-table indices, and V3 audited clip space and + sRGB. `#248` came out of the V3 audit. - **Deferred Linux graphical track:** L0 completed at `66f114b2`; L1's backend selection/capability implementation checkpoint landed at `11501d52`. Native Windows passes and WSLg correctly rejects its missing mandatory @@ -92,6 +97,42 @@ Copy this block when adding a new issue: --- +## #248 — FrustumCuller extracts the near plane with the GL-convention formula + +**Status:** OPEN +**Severity:** LOW (correctness hygiene; not currently exploitable) +**Filed:** 2026-07-27 +**Component:** rendering / culling + +**Description:** `FrustumCuller.FromViewProjection` +(`src/AcDream.App/Rendering/FrustumCuller.cs:34-55`) extracts the near plane as +`Normalize(col4 + col3)` — the classic Gribb-Hartmann formula for OpenGL's +`[-1,1]` NDC z range. Every acdream projection is built by +`Matrix4x4.CreatePerspectiveFieldOfView`, whose NDC z range is `[0,1]`, for which +the correct near-plane extraction is `col3` alone. + +**Why it is not currently a bug:** the mismatched formula places the effective +near threshold at `-n·f/(2f-n)` — roughly 0.5 m instead of 1.0 m for the retail +chase camera — which makes the AABB test strictly *more* permissive near the eye. +It can keep something the true frustum would drop, never the reverse, so it +produces no missing geometry. The far plane (`col4 - col3`) is identical under +both conventions and is unaffected. + +**Provenance:** found by the Campaign V slice V3 clip-space audit. This is the +same class of mistake as the `PortalProjection` near-test bug documented at +`src/AcDream.App/Rendering/PortalProjection.cs:12-19`, which *was* user-visible +(it clipped a doorway the camera stood close to, culling the cell behind it). +Filed rather than fixed inline because it is pure CPU math, untouched by the +OpenGL → Vulkan migration, and therefore outside Campaign V's scope. + +**Fix:** extract `near = Normalize(col3)`. + +**Acceptance:** culling behaviour unchanged in the offline pixel gate and the +connected route; a unit test pinning the extracted near-plane distance to the +camera's actual near value. + +--- + ## #247 — Loot ordering and local dropped-item projection regressed **Status:** DONE — 2026-07-26; connected loot/drop/selection gate passed diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index 86d56d2e..507a4414 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -240,8 +240,9 @@ straight into `GpuMemoryTracker` for exact accounting, which VMA would obscure. write-to-array-then-`BufferSubData` (driver validation, copy, rename tracking) simply stops existing. - **Textures** — device-local pool. Formats stay UNORM (`BC1/2/3_UNORM`, - `R8G8B8A8_UNORM`, `R8_UNORM`); sRGB correctness lives at the framebuffer, as - it does on GL today. 2D arrays are allocated full-size and filled + `R8G8B8A8_UNORM`, `R8_UNORM`), matching GL exactly. There is no sRGB anywhere + in the pipeline — not on upload, not in the shaders, not at the framebuffer + (V3 audit, §4.10). 2D arrays are allocated full-size and filled incrementally, mirroring `ManagedGLTextureArray`. - **Mip generation** — DAT surfaces ship no mips. Uncompressed formats get a `vkCmdBlitImage` chain at upload. **BC formats cannot be blit targets**, so @@ -355,16 +356,76 @@ Budget: roughly 4–6 batched `vkCmdPipelineBarrier2` calls per frame. Surface through Silk windowing (`GraphicsAPI.DefaultVulkan`, `IWindow.VkSurface`) so the existing GLFW platform selection, `ACDREAM_DISPLAY_PROTOCOL`, and window -lifecycle are unchanged. Format `B8G8R8A8_SRGB` preferred (matching the GL -`FramebufferSrgb` contract: shaders write linear, the attachment encodes); -screenshots swizzle BGRA→RGBA on the CPU to preserve +lifecycle are unchanged. Format **`B8G8R8A8_UNORM`** — see §4.10, this was +corrected at V3 and is the single highest-severity finding of the audit. +Screenshots swizzle BGRA→RGBA on the CPU to preserve `FrameScreenshotController`'s RGBA byte contract. Present modes: `FIFO` when VSync is on; `IMMEDIATE` preferred then `MAILBOX` when off, with `FramePacingController` and its platform waiters continuing to drive the software cap. `OUT_OF_DATE` recreates immediately, `SUBOPTIMAL` at the next frame boundary, both through `FramebufferResizeController`. -### 4.10 Capability gate +### 4.10 V3 audit findings (2026-07-27) + +Slice V3 audited every projection producer, every depth-range assumption, the +clip-plane derivation, the sRGB path, and MSAA control. The central claim held — +**but one plan assumption was wrong, and catching it is why V3 exists.** + +**Confirmed.** Every projection that reaches a shader is built by +`Matrix4x4.CreatePerspectiveFieldOfView` (world, portal tunnel, paperdoll, +appraisal cameras; terrain, mesh, particles, debug lines and sky all consume the +same matrices). There are **no orthographic projections in production code at +all** — the retained UI's `ui_text.vert` converts pixel coordinates straight to +NDC with a constant `z = 0`, so V4a has no matrix to convert, only a Y-sign to +check. So: no projection rework, exactly as designed. + +`SkyProjection.WithDepthRange` is the only hand-written matrix edit, assigning +`M33`/`M43` directly. It re-derives the *same* D3D-convention near/far mapping +(it even throws on an orthographic input) rather than a GL-style `2/(f-n)` +scale — correct, but the sharpest edge in the codebase and a required +cross-check at V6. + +Phase U.3's clip planes are derived and consumed entirely in clip space with +`plane.z` always 0, so they are insensitive to both the depth convention and the +viewport Y flip. No change needed. + +**Corrected — sRGB.** The plan previously specified a `B8G8R8A8_SRGB` swapchain +"matching the GL `FramebufferSrgb` contract." That contract does not exist. +`EnableCap.FramebufferSrgb` is enabled only inside the throwaway 2×2 capability +probe (`GraphicalGlFunctionProbe.cs:419-429`) and disabled immediately; it is +never enabled on the real backbuffer. No texture is uploaded in an sRGB internal +format (`TextureFormatExtensions` has none), and no shader performs any gamma +conversion. The renderer is plain UNORM end to end. **The correct Vulkan +swapchain format is `B8G8R8A8_UNORM`**; shipping `_SRGB` would have applied an +unwanted encode to already-display-space values — a global brightening across +every frame, and precisely the failure mode §6 lists as "cannot pass silently." +It would have passed silently right up to V7. + +Separately: the capability gate *requires* sRGB-framebuffer support that the +renderer never uses. Harmless today, but the Vulkan gate must not carry the +stale requirement forward. + +**MSAA.** `ACDREAM_MSAA_SAMPLES` overrides the quality preset +(`QualityPreset.cs:43-59`) and `0` forces MSAA off, but it is read at window +creation and cannot change mid-session. The V7 differential script must therefore +*launch* both backends with `ACDREAM_MSAA_SAMPLES=0` rather than toggling a +setting. + +**Two concrete acceptance items carried to V6/V7.** + +1. **Scissor Y convention.** `NdcScissorRect.ToPixels` emits GL bottom-left-origin + pixel rectangles. Vulkan's `vkCmdSetScissor` is always top-left-origin — the + negative viewport height does *not* flip the scissor. The contract already says + callers keep GL convention and the backend converts + (`IGpuPassEncoder.SetScissor`), so the Vulkan encoder must do that flip. A + scissored aperture — a doorway — is the right differential-gate target. +2. **`FrustumCuller` near plane** extracts `col4 + col3`, the GL `[-1,1]` Gribb- + Hartmann formula, against `[0,1]`-convention matrices; the correct extraction + is `col3` alone. Proven over-inclusive rather than over-culling, so it is not a + visibility bug, and it is pure CPU math untouched by the backend swap. Filed as + a tracked issue rather than fixed here — it is not Campaign V's scope. + +### 4.11 Capability gate Mirrors the GL three-layer shape exactly — passive record, **active** probes, an `Evaluate` that throws `NotSupportedException` → `Program.cs` exit code 4 → an