feat(render): Campaign V slice V4t-1 — terrain crosses to GpuTextureSlot
V4t moves the world texture stack off the raw 64-bit ARB_bindless_texture handle and onto GpuTextureSlot. This first commit does terrain only, because terrain is the one branch of that stack whose producer and consumer are a single pair — TerrainAtlas and TerrainModernRenderer — so it can carry the new device seam on its own pixel gate before the mesh/composite/particle retype lands on top of it. Why the device's table can now be reached, when §5.2 said it could not. That paragraph's reason was the flush: GlGpuDevice drains its dirty table runs inside FlushBeforeDraw, which only an encoder-recorded draw reaches, so a raw-GL renderer would sample a stale table. §5.5.6 then closed the GL re-land of V4c/V4d, which means the world renderers stay raw GL through to V10 — so "wait for the encoder" stopped being a plan and became an indefinite block on V4t, which the Vulkan world arm cannot be written without. The resolution is the smallest one that keeps the seam honest: the drain is factored out as GlGpuDevice.FlushTextureTable, and a raw-GL renderer calls it and binds TextureTableGlName at binding 9 itself, immediately before its own draw — the same shape its retired private GlBindlessHandleTable had, against a table that is now the device's. Nothing else of the backend is exposed, and both members are deleted with the raw-GL world path. Residency ownership deliberately does NOT move. RegisterWorldTextureHandle interns an already-resident handle and owns only the table entry; the atlas still creates, makes resident and destroys its own textures. That is what separates it from RegisterTexture, which owns the residency it creates, and it is why this slice can retype the data model without also porting GL texture creation onto IGpuTexture. TerrainAtlas.GetBindlessHandles becomes GetTextureSlots(GlGpuDevice). Registration is idempotent by handle, so the per-draw call is two dictionary lookups — the cadence GetOrAdd already had. It is conditional on the handle having changed because SetAnisotropic makes both textures non-resident and re-acquires them: without that check a quality-preset change would strand a slot holding a non-resident handle, so the superseded entry is retired in the same step through the device's retirement queue. Ordering is unaffected. Terrain's two slots travel as loose uniforms (uTextureIndexA/B) and enter no sort and no bucket key, so a different slot NUMBER changes nothing about what is drawn or in what order — only which table index resolves to the same handle. Gates. GL offline pixel gate vscb2a70b8: 3.02e-05 (17 of 563,200 pixels), exactly a same-commit control value and inside the documented 15-23 px / <=4.1e-05 band. tools/run-repeat-connected-gate.ps1 -Runs 3: 3/3 RENDERED on both the desktop witness and the client capture. One Vulkan composition-host run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero errors, zero warnings, empty validation log, converged ownership ledger. App tests 4,075 / 3 skips (#250's zero-allocation test reran green singly). One connected run of an earlier 3-run attempt died in the render loop with "OpenGL returned unexpected fence wait status NoError (0x0)" from GpuFrameFlightController.RetireFence. It did not reproduce in the following three runs at this tree nor in three interleaved runs atcb2a70b8, and this diff creates, deletes and waits on no fence. Filed as #251 rather than attributed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cb2a70b867
commit
b8bcaa3ef2
6 changed files with 228 additions and 83 deletions
|
|
@ -97,6 +97,63 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #251 — glClientWaitSync returned 0 and crashed the render loop, once in nine connected runs
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** MEDIUM (one observed occurrence; kills the process when it fires)
|
||||
**Filed:** 2026-07-28
|
||||
**Component:** rendering / GL frame-flight fences
|
||||
|
||||
**Description:** During the Campaign V slice V4t connected gate, one run died
|
||||
with an unhandled `InvalidOperationException` in `OnRender`:
|
||||
|
||||
```
|
||||
OpenGL returned unexpected fence wait status NoError (0x0).
|
||||
at GpuFrameFlightController.RetireFence(Int32 slot)
|
||||
at RenderFrameOrchestrator.Render(RenderFrameInput input)
|
||||
at GameWindow.OnRender(Double deltaSeconds)
|
||||
```
|
||||
|
||||
`glClientWaitSync` is specified to return `ALREADY_SIGNALED`,
|
||||
`TIMEOUT_EXPIRED`, `CONDITION_SATISFIED` or `WAIT_FAILED`. It returned 0, which
|
||||
is none of those, so `SilkGpuFenceApi.Wait`'s exhaustive switch threw — the
|
||||
switch is correct and the throw is the right behaviour; the anomaly is the
|
||||
driver's return value. The run had already reached `world-visible` and
|
||||
`complete` and had logged a graceful logout; the crash landed while it was
|
||||
still rendering, before the probe's screenshot, so the gate recorded
|
||||
`NO-CAPTURE`. Shutdown then reported
|
||||
`status=AbandonedIncomplete, blocked=submitted GPU work` — the same fence, hit
|
||||
a second time from `WaitForSubmittedWork` — followed by Silk.NET's
|
||||
"You cannot call `Reset` inside of the render loop!" from the native fallback.
|
||||
Both are consequences, not separate defects.
|
||||
|
||||
**Root cause / status:** Unknown, and NOT attributed to V4t. It occurred once
|
||||
in nine connected runs on 2026-07-28: once in three at the V4t-1 tree, then
|
||||
zero in three more at that same tree and zero in three interleaved runs at
|
||||
`cb2a70b8`. The V4t-1 diff creates, deletes and waits on no fence and adds no
|
||||
retirement registration that executes on that path. A sync object whose handle
|
||||
stops being valid mid-session, on a clean `glGetError`, is the same
|
||||
below-the-API failure family the campaign documented four instances of on this
|
||||
exact driver (AMD 26.6.4, RX 9070 XT) in plan §5.5.1–§5.5.3 — a deadlocking
|
||||
`glGetQueryObject` read, a never-executed `GL_QUERY_BUFFER` write, a
|
||||
multisampled `glReadPixels`, and a capture that could not see the presented
|
||||
surface. That is a hypothesis, not a finding: nothing here rules out a real
|
||||
double-delete or a lifetime bug in our own fence bookkeeping.
|
||||
|
||||
**Files:**
|
||||
|
||||
- `src/AcDream.App/Rendering/GpuFrameFlightController.cs:274` `RetireFence`
|
||||
- `src/AcDream.App/Rendering/GpuFrameFlightController.cs:474` `SilkGpuFenceApi.Wait`
|
||||
- `src/AcDream.App/Rendering/GameWindowLifetime.cs:419` shutdown's `frame flight drain`
|
||||
|
||||
**Acceptance:** Either a reproduction that pins the invalidation to our own
|
||||
bookkeeping and a fix for it, or — if the driver is confirmed — a decision
|
||||
recorded here about whether a 0 return should be treated as `WAIT_FAILED` and
|
||||
retried rather than thrown. Do not silently widen the switch to swallow it: an
|
||||
unexpected status is exactly the signal §5.5 spent three days wishing it had.
|
||||
|
||||
---
|
||||
|
||||
## #250 — Zero-allocation tests fail intermittently, roughly 1 run in 3
|
||||
|
||||
**Status:** OPEN
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue