Add optional BindlessSupport ctor parameter + GetBindlessHandles()
method that returns (terrainHandle, alphaHandle) ulongs with both
textures made resident. Two-phase Dispose mirroring TextureCache
(MakeNonResident before DeleteTexture per ARB_bindless_texture spec).
Existing callers pass `Build(gl, dats)` unchanged; bindless = null
default keeps them working until T6/T8 wires the renderer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweeps all (lbX, lbY, cellX, cellY) tuples for the full 255x255
landblock map (~4.16M cells) and reports both the raw enum-output
disagreement (50.02%) and the diagonal-actually-painted disagreement
(49.98%) between WB's CalculateSplitDirection and acdream's
TerrainBlending.CalculateSplitDirection (which retail uses per
CLandBlockStruct::ConstructPolygons at retail addr 00531d10).
The two formulas behave like independent random hashes. Adopting
WB's pipeline wholesale would mis-render ~half the diagonals on
every landblock (Holtburg 0xA9B0: 29/64 cells = 45.3% wrong). This
data is the foundation for N.5b's Path A vs B vs C decision (kills
Path A).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures everything a fresh agent needs to pick up Phase N.5b (Terrain
on the Modern Rendering Path) without spelunking through the N.5
session history.
Front-loads the load-bearing constraint: issue #51 (WB's terrain split
formula diverges from retail's FSplitNESW). Lays out three viable
design paths (A: adopt WB's formula everywhere; B: keep retail's
formula and fork-patch WB; C: WB mesh layout but our formula). The
brainstorm needs to pick one, informed by quantified divergence rate
across representative landblocks.
Includes file-by-file inventory of acdream's terrain stack (1383 lines
across TerrainRenderer + TerrainChunkRenderer + TerrainAtlas + shaders)
vs WB's (1937 lines across TerrainRenderManager + TerrainGeometryGenerator
+ LandSurfaceManager). Eight brainstorm questions covering atlas model,
mesh ownership, index format, shader unification, streaming integration,
conformance test, and visual verification gate.
Mirrors the N.5 handoff structure that worked well last session:
TL;DR + where N.5 left things + what N.5b inherits + technical detail
+ files to read + brainstorm questions + acceptance criteria + first
30 minutes + things to NOT do.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records a new Phase A sub-piece: split the single ACDREAM_STREAM_RADIUS
into separate terrain + entity radii so terrain renders to a much
further horizon (WB-style) while entities/scenery stay at the current
closer radius.
Motivated by perf at ACDREAM_STREAM_RADIUS=5 dropping from ~810 fps
to ~200-300 fps because everything stays full-detail. Both retail and
WorldBuilder render terrain way out and strip entities at distance.
Estimate: 3-5 days for the radius split + fog tuning; +1 week if
terrain LOD via mesh decimation is included. Not yet brainstormed.
N.8 (sky + particles via WB's SkyboxRenderManager + ParticleEmitterRenderer)
was already on the roadmap; user confirmed they want it tracked there.
No edit needed for N.8 — already at the right level of detail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reframe the selection-blink follow-up so it doesn't suggest near-term
work. Was listed in N.5 ship record as "Phase B.4 follow-up adds the
field" — now phrased as open backlog with the hook reserved in
mesh_modern.vert's InstanceData comment for whoever eventually picks
it up.
The shader hook itself is unchanged — change is purely doc wording in
the plan SHIP record + CLAUDE.md WB integration cribs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
N.5: Modern Rendering Path. WbDrawDispatcher now uses bindless
textures + glMultiDrawElementsIndirect on top of N.4's grouped
pipeline. Three SSBO uploads + 2 indirect calls per frame, ~12-15
total GL calls for entity rendering regardless of scene complexity.
Measured 1.23 ms / frame median at Holtburg courtyard (1662 groups,
~810 fps). User-gated visual verification PASS at Holtburg.
Includes ship-amendment: legacy renderer path formally retired
(InstancedMeshRenderer + StaticMeshRenderer + WbFoundationFlag
deleted). Bindless is now mandatory; missing extensions throw
NotSupportedException at startup with a clear error message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Corrects the SHIP commit's acceptance gate verdict on the legacy
escape hatch. The original gate "[x] ACDREAM_USE_WB_FOUNDATION=0
still works" was inaccurate — Task 15's mesh_instanced deletion left
InstancedMeshRenderer orphaned + non-functional. Resolution: formal
retirement of the legacy path within N.5 (the prior commit).
Updated acceptance gate verdict:
- [N/A] ACDREAM_USE_WB_FOUNDATION=0 — escape hatch retired in N.5;
modern path is now mandatory, bindless required at startup. Missing
bindless throws NotSupportedException with a clear error message.
All other gates unchanged from the SHIP commit:
- [x] Visual identity to N.4 — Task 10 + Task 14 USER GATE PASS
- [x] CPU dispatcher time <= 70% of N.4 — measured 1.23 ms/frame at
Holtburg courtyard, comfortably under threshold
- [x] drawsIssued <= 5 per pass (CPU GL calls) — 2 indirect calls/frame
- [x] All tests green — 71/71 in the relevant filter
- [ ] GPU rendering time +-10% of N.4 — DEFERRED (timer query
double-buffering, N.6 follow-up)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final cross-cutting review of N.5 found that Task 15's deletion of
mesh_instanced.vert/.frag left InstancedMeshRenderer orphaned —
ACDREAM_USE_WB_FOUNDATION=0 silently rendered terrain+sky only with
no entities. The SHIP commit's "[x] ACDREAM_USE_WB_FOUNDATION=0 still
works" claim was inaccurate.
Resolution: formal retirement of the legacy renderer path within N.5
instead of deferring to N.6.
Deleted:
- src/AcDream.App/Rendering/InstancedMeshRenderer.cs
- src/AcDream.App/Rendering/StaticMeshRenderer.cs
- src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs
GameWindow simplified — capability detection is unconditional, missing
bindless throws NotSupportedException with a clear message at startup.
WbDrawDispatcher + mesh_modern shader load are mandatory after init.
No escape hatch.
GpuWorldState simplified — WbFoundationFlag.IsEnabled guards on
AddLandblock/RemoveLandblock removed; adapter calls are unconditional
when the adapter is non-null.
PendingSpawnIntegrationTests updated — WbFoundationFlag.ForTestsOnly_ForceEnable
static ctor removed (flag is gone; adapter calls are unconditional).
The ApplyLoadedTerrain physics-data loop was also simplified: the
EnsureUploaded sub-loop that fed InstancedMeshRenderer is gone;
_pendingCellMeshes is now explicitly cleared to prevent unbounded
accumulation (the worker thread still populates it, but WB handles
EnvCell geometry through its own pipeline).
Spec §2 Decision 5 + §10 Out-of-Scope updated. Plan ship-amendment
section added. Roadmap updated (N.5 ships with retirement; N.6 scope
narrowed to perf-only). CLAUDE.md "WB integration cribs" updated.
Perf baseline doc updated. WbDrawDispatcher class summary docstring
corrected to describe the as-shipped SSBO + multi-draw-indirect path.
ISSUES.md #51 updated (terrain not in N.5 scope; deferred to N.7).
Bindless support is now a hard requirement. Modern desktop GPUs
universally expose GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters;
if a user hits the NotSupportedException, that's a real bug report
worth investigating, not a silent fallback.
Build: 0 errors, 0 warnings. Tests: 71/71 (Wb+MatrixComposition+TextureCacheBindless filter).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bindless textures + glMultiDrawElementsIndirect on top of N.4's grouped
pipeline. Per-frame entity rendering: 3 SSBO uploads (instance matrices
@ binding=0, batch data @ binding=1, indirect commands) + 2 indirect
calls (opaque + transparent). Total ~12-15 GL calls per frame for entity
rendering, regardless of scene complexity.
Acceptance gates (spec §8.3):
- [x] Visual identity to N.4 — Task 10 USER GATE PASS (Holtburg courtyard)
+ Task 14 USER GATE PASS (general roaming, no regressions seen)
- [x] CPU dispatcher time ≤ 70% of N.4 — measured 1.23 ms/frame median
at Holtburg courtyard (1662 groups, ~810 fps); estimated N.4
hot path ≥2.5 ms/frame; comfortably under threshold
- [x] drawsIssued ≤ 5 per pass (CPU GL calls) — exactly 2 indirect calls
per frame regardless of scene size
- [x] All tests green — 71/71 in
FullyQualifiedName~Wb|FullyQualifiedName~MatrixComposition|FullyQualifiedName~TextureCacheBindless
- [x] ACDREAM_USE_WB_FOUNDATION=0 still works — InstancedMeshRenderer
escape hatch preserved (its own shader path, untouched)
- [ ] GPU rendering time within ±10% of N.4 — DEFERRED to N.6.
GL_TIME_ELAPSED query polling never reports avail!=1 within the
same frame; needs double-buffering. CPU is the load-bearing metric.
Plan amendments captured during execution:
- Task 2: parallel Texture2DArray upload path (replacing the original
"switch globally" framing that would've broken 4 legacy consumers)
- Task 3+4: parallel bindless cache dictionaries (avoiding the GLSL
type mismatch from sampling a Texture2D handle via sampler2DArray)
- Task 5: preserved mesh_instanced.frag's full SceneLighting UBO + 8
lights + fog + lightning flash + per-channel clamp
- Task 9: BatchDataPublic Pack=8 (required for safe MemoryMarshal.Cast)
Plan archived at:
docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md
Spec at:
docs/superpowers/specs/2026-05-08-phase-n5-modern-rendering-design.md
Perf baseline at:
docs/plans/2026-05-08-phase-n5-perf-baseline.md
Memory at:
~/.claude/.../memory/project_phase_n5_state.md
Files changed: 6 added, 6 modified, 2 deleted. 19 tasks shipped across
~40 commits including amendments + fixups + reviews.
N.6 follow-ups: retire InstancedMeshRenderer entirely; GPU timer query
double-buffering; persistent-mapped buffers if profiling shows the
residual glBufferData hot spot; possible WB atlas adoption for memory
savings on shared content; possible GPU-side culling via compute pre-pass;
per-instance highlight (selection blink) for retail-faithful click feedback
(field reserved in mesh_modern.vert's InstanceData struct).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves N.5 from in-flight to Shipped (2026-05-08). N.6 (retire
InstancedMeshRenderer + perf polish) becomes the in-flight phase.
CLAUDE.md in-flight pointer updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the as-shipped state: acceptance gate verdicts, plan amendments
captured during execution, code-review adjustments per task, out-of-scope
N.6 follow-ups, and a complete files-changed summary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mesh_instanced.vert + .frag deleted. WbDrawDispatcher always uses
mesh_modern when WB foundation is on. Legacy escape hatch
(ACDREAM_USE_WB_FOUNDATION=0 or bindless missing) runs through
InstancedMeshRenderer which has its own shader path — untouched.
GameWindow's else-branch removed; if bindless is missing, _meshShader
stays unloaded, _wbDrawDispatcher stays null, and _staticMesh is not
constructed (its guard requires _meshShader non-null). All downstream
_staticMesh usages were already null-safe (null-conditional operators
or explicit null guards). Two null-forgiving suppressors added at the
WbDrawDispatcher + SkyRenderer construction sites where the compiler
couldn't prove non-null but the logic guarantees it (both require
_bindlessSupport non-null, which implies _meshShader was assigned;
_textureCache is assigned unconditionally).
InstancedMeshRenderer.cs: the one reference to mesh_instanced was
a code comment (location 3 NOT used by mesh_instanced.vert) — not
a file load. Escape hatch code path is preserved; the shader comment
is now stale but low priority.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CPU dispatcher: 1227 µs / frame median (1303 µs p95) at Holtburg
courtyard, 1662 groups in working set. Inferred ~810 fps sustained.
CPU dispatcher acceptance gate (≤70% of N.4): PASS — N.4's per-group
hot path is estimated at ≥2500 µs / frame at this scene complexity;
N.5 is comfortably under half.
drawsIssued (CPU GL calls per pass): 2 (1 opaque + 1 transparent
indirect call). Down from N.4's ~hundreds per pass. PASS.
GPU timing: unmeasured. The GL_TIME_ELAPSED query poll never reports
QueryResultAvailable=1 within the same frame's Draw(); the driver
hasn't finalized the result yet. Fix is double-buffering (queryA
on frame N, read on N+2). Deferred to N.6 perf polish — doesn't block
N.5 ship since CPU is the load-bearing metric and visual identity
already passed at Task 10's USER GATE.
Direct N.4 baseline NOT measured. Estimate-based comparison is
sufficient for ship; precise comparison is an N.6 follow-up.
Baseline doc at docs/plans/2026-05-08-phase-n5-perf-baseline.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds median + 95th-percentile CPU + GPU dispatch time to the existing
5-second [WB-DIAG] rollup. CPU via Stopwatch (always running, cheap;
only logged under ACDREAM_WB_DIAG=1). GPU via two GL_TIME_ELAPSED
queries (opaque + transparent) wrapping each glMultiDrawElementsIndirect,
polled non-blocking via QueryResultAvailable on the next frame.
Sample window is 256 frames per signal; median + p95 reported.
Numbers populate the SHIP commit's perf table at Task 19.
Silk.NET naming note: GL_TIME_ELAPSED queries use QueryTarget.TimeElapsed
(confirmed present in Silk.NET.OpenGL 2.23.0 DLL). The 64-bit result is
read via GetQueryObject(..., out ulong) which dispatches to
glGetQueryObjectui64v; the int overload (glGetQueryObjectiv) is used for
the ResultAvailable poll, matching WorldBuilder's VisibilityManager pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks in Decision 2 (Opaque + ClipMap → opaque indirect; AlphaBlend +
Additive + InvAlpha → transparent indirect). Catches future refactors
that drift the partition — silent visual regression otherwise (groups
rendered in the wrong pass with the wrong blend state).
Adds public static IsOpaquePublic shim on WbDrawDispatcher; the
underlying IsOpaque stays private.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces WbDrawDispatcher's per-group glDrawElementsInstancedBaseVertexBaseInstance
loop with two glMultiDrawElementsIndirect calls (opaque + transparent).
Per-frame uploads three SSBOs:
- _instanceSsbo @ binding=0 (mat4 per instance, indexed by gl_BaseInstanceARB + gl_InstanceID)
- _batchSsbo @ binding=1 (BatchData per group, indexed by gl_DrawIDARB)
- _indirectBuffer (DrawElementsIndirectCommand[] — opaque first, transparent second)
GameWindow swaps the shader load to mesh_modern when _bindlessSupport
is non-null. Capability detection + shader load now run in the right
order (capability before TextureCache + before Shader).
Deletes the obsolete DrawGroup stub, EnsureInstanceAttribs, _instanceBuffer,
_patchedVaos. ClassifyBatches + ResolveTexture already migrated in
Task 8 to use ulong bindless handles.
BuildIndirectArrays (Task 9) wired in: _opaqueDraws + _translucentDraws
are flattened into IndirectGroupInput[], laid out via the helper into
contiguous indirect commands + parallel BatchData[]. opaqueByteOffset=0,
transparentByteOffset = opaqueCount × DrawCommandStride.
Visual verification (USER GATE) PASS: Holtburg courtyard renders
identical to N.4 — terrain, scenery, characters, NPCs all visible
without artifacts. [N.5] modern path capabilities present + mesh_modern
shader loaded log lines confirm the boot path. [WB-DIAG] hot-path
counters show healthy entity/draw activity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught:
- sizeofDEIC was a local; promoted to public const DrawCommandStride
so tests can reference it symbolically.
- BatchDataPublic layout invariant (size + field offsets) wasn't
asserted in tests. Added BatchDataPublic_LayoutMatchesPrivateBatchData
+ DrawCommandStride_MatchesStructSize tests to gate Task 10's
MemoryMarshal.Cast<BatchData, BatchDataPublic> safety.
- Plan doc updated: BatchDataPublic spec was Pack=4 (wrong — must
match private BatchData's Pack=8 for the cast to work). Implementation
was already correct; plan now matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure CPU helper that lays out a group list into a contiguous indirect
buffer (DrawElementsIndirectCommand[]) and parallel BatchData[] —
opaque section first, transparent section second. Returns counts +
byte offset for the transparent section.
Tests cover: spec §5 walk-through layout; empty group list edge case;
ClipMap classification (treated as opaque, not transparent).
Static + public so tests can exercise without a GL context. Task 10
wires it into the rewritten Draw() method.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces uint TextureHandle (32-bit GL name) with ulong
BindlessTextureHandle (64-bit) in InstanceGroup + GroupKey + ResolveTexture
return type. Adds TextureLayer (always 0 for per-instance composites,
becomes meaningful when WB atlas is adopted in N.6).
ClassifyBatches now calls TextureCache.GetOrUpload*Bindless variants —
these return Texture2DArray-backed bindless handles (Task 3 work).
DrawGroup body throws NotImplementedException — Task 10 rewrites the
whole Draw() method to use glMultiDrawElementsIndirect, which makes
DrawGroup obsolete. CPU-only tests don't invoke DrawGroup so the build
+ test gates stay green; visual launch fails until Task 10 (intentional).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught that BatchData uses Pack=4 but contains a
ulong field. With the current field order (TextureHandle first), offset
0 is always 8-byte aligned so std430 works. But adding a 4-byte field
before TextureHandle without bumping Pack would silently misalign the
GPU struct. Pack=8 makes the alignment requirement explicit and adds
a comment documenting expected std430 offsets.
No runtime change — current offsets (0/8/12) are identical under both
Pack values for this field order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds DrawElementsIndirectCommand struct (20-byte layout for
glMultiDrawElementsIndirect). Replaces _instanceVbo field on
WbDrawDispatcher with three buffers: _instanceSsbo (mat4[]),
_batchSsbo (BatchData[]), _indirectBuffer (DEIC[]). Adds BindlessSupport
constructor parameter — non-null required since the dispatcher is only
constructed when WB foundation is on (which implies bindless is present
per Task 6 capability detection).
Existing Draw() method substitutes _instanceVbo -> _instanceSsbo for
compile. Behavior is temporarily wrong (SSBO bound as ArrayBuffer for
per-vertex attribs); Tasks 9-10 fully rewrite the draw loop and the
per-frame uploads to use BindBufferBase + glMultiDrawElementsIndirect.
GameWindow construction site updated to add _bindlessSupport guard and
pass it as the new last argument to the constructor. Dispatcher is only
constructed when bindless is guaranteed present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught:
- Silent failure when ARB_bindless_texture absent — the && short-circuit
meant the most common fallback case (no bindless on the GPU) had no
log, while ARB_shader_draw_parameters absent did log. Restructured to
three nested ifs so each failure path logs symmetrically.
- Redundant `bindless is not null` guard removed (TryCreate's non-null
guarantee covers it; the nested-if structure makes this implicit).
- HasShaderDrawParameters in BindlessSupport.cs replaced its manual
GL_NUM_EXTENSIONS scan with `gl.IsExtensionPresent(...)` — same
pattern WB uses, less code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detects ARB_bindless_texture + ARB_shader_draw_parameters at startup
when WbFoundationFlag is enabled. Stores BindlessSupport on GameWindow
and passes it to TextureCache so the parallel Texture2DArray upload
path is available to future bindless callers.
Mesh shader load remains mesh_instanced for now — Task 10 swaps to
mesh_modern after Tasks 7-9 rewire the dispatcher to consume the
bindless + SSBO + indirect machinery.
Capability missing → BindlessSupport stays null → TextureCache runs
without the bindless path → legacy callers (StaticMeshRenderer,
InstancedMeshRenderer, ParticleRenderer, current WbDrawDispatcher
draw loop) are unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught four issues:
- Unnecessary GL_ARB_bindless_texture extension in mesh_modern.vert
(vert doesn't use bindless types). Removed; only the frag needs it.
- SSBO binding=1 (BatchBuffer) and UBO binding=1 (SceneLighting) are
in distinct GL namespaces — added a comment in the vert documenting
this so Task 10's bind site doesn't get confused.
- Misleading "0=opaque, 1=transparent" comment expanded to spell out
the full Decision 2 two-pass alpha-test logic and what each discard
threshold protects against.
- BatchData.flags field is reserved; documented that N.5's dispatcher
owns all blend state, with a hook for future shader-side additive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New entity shaders for the WB modern rendering path. Modeled on WB's
StaticObjectModern.* but adapted to acdream's lighting model:
- Drops uActiveCells (we cull cells on CPU in WbDrawDispatcher)
- Drops uDrawIDOffset (full passes, no pagination)
- Drops uHighlightColor (deferred to Phase B.4 follow-up; field reserved
in InstanceData struct comment)
- Preserves mesh_instanced's SceneLighting UBO at binding=1 with 8 lights,
fog params, lightning flash, per-channel clamp — full visual identity
vert reads InstanceData[] @ binding=0 indexed by gl_BaseInstanceARB +
gl_InstanceID for the per-entity model matrix; reads BatchData[] @
binding=1 indexed by gl_DrawIDARB for the per-group bindless texture
handle + layer.
frag samples sampler2DArray reconstructed from a uvec2 bindless handle
+ uint layer. uRenderPass uniform picks two-pass alpha-test thresholds:
0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95
and alpha<0.05).
Not yet wired to the dispatcher — Task 6 sets up shader load + capability
detection in GameWindow; Task 7-10 rewrite the dispatcher to use SSBO +
glMultiDrawElementsIndirect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original Task 5 draft used hardcoded vec3 ambient/sun uniforms in
mesh_modern.frag. Reading actual mesh_instanced.frag revealed it uses
a SceneLighting UBO at binding=1 with 8 lights, fog params (start/end/
lightning/mode), fog color, camera/time, and per-channel clamp.
Revised: mesh_modern.frag preserves the full SceneLighting UBO +
accumulateLights + applyFog + lightning flash + per-channel clamp.
mesh_modern.vert adds vWorldPos output (consumed by accumulateLights
and applyFog). Visual identity to N.4's lighting model preserved.
Two-pass alpha-test (N.5 Decision 2) sits inside the same shader,
gated by uRenderPass instead of uTranslucencyKind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught four issues:
- Critical: Dispose interleaved MakeNonResident + DeleteTexture per
entry, violating ARB_bindless_texture's "all handles non-resident
before any texture deletion" requirement. Reordered to two phases:
Phase 1 makes ALL bindless handles non-resident; Phase 2 deletes
ALL bindless textures; Phase 3 deletes legacy Texture2D textures.
- Important: per-call _bindless?.MakeNonResident replaced with a
single if (_bindless is not null) guard around the whole Phase 1
block — cleaner reasoning, one null check.
- Minor: test contract comment referenced wrong task number for
visual gate; corrected to match current plan.
- Minor: two abbreviated XML docs (GetOrUploadWithOrigTextureOverrideBindless,
GetOrUploadWithPaletteOverrideBindless) expanded to mention the
throw-on-null-bindless contract for IDE readers.
This fixup also completes Task 4's Dispose work — Task 4 will be
marked complete since this commit does its full job.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three Bindless variants (GetOrUploadBindless,
GetOrUploadWithOrigTextureOverrideBindless,
GetOrUploadWithPaletteOverrideBindless) that decode + upload via
UploadRgba8AsLayer1Array (Texture2DArray) and cache in three new
dictionaries that mirror the legacy three-cache structure. Each entry
stores both the GL texture name (for Dispose cleanup in Task 4) and
the resident bindless handle.
Constructor gains optional BindlessSupport param; null keeps backward
compat. EnsureBindlessAvailable throws InvalidOperationException if
Bindless* methods are called without BindlessSupport (fail-fast vs
silent zero handle that would produce GPU faults).
Dispose extended to make handles non-resident before deleting the
underlying Texture2DArray names (bindless handles must be made
non-resident before the texture is deleted; skipping this causes
GPU faults on driver cleanup).
Marker test in TextureCacheBindlessTests documents the throw contract
for future engineers; real bindless integration is verified at
Task 14's visual gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original Task 3 had Bindless* methods calling the legacy Texture2D
GetOrUpload* and converting the GL name to a bindless handle —
producing a sampler2D texture sampled via sampler2DArray (GLSL type
mismatch).
Revised: Task 3 introduces three parallel cache dictionaries
(_bindlessBySurfaceId / _bindlessByOverridden / _bindlessByPalette)
storing both the GL texture name and the resident handle. Bindless*
methods call DecodeFromDats + UploadRgba8AsLayer1Array directly with
their own caching; legacy three-cache structure mirrored exactly.
Task 4 (Dispose) updated to: (1) MakeNonResident on every bindless
handle FIRST, (2) DeleteTexture on every Texture2DArray name, (3)
DeleteTexture on every legacy Texture2D handle. Order matters per
ARB_bindless_texture spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught that the TexImage3D call dropped the
depth: and border: named arguments specified in the plan. The bare
positional `1, 0` is hard to disambiguate from the surrounding 10
parameters. Adds them back, no runtime change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds UploadRgba8AsLayer1Array — uploads pixel data as a 1-layer
Texture2DArray. Existing UploadRgba8 (Texture2D) untouched, so legacy
callers (StaticMeshRenderer, InstancedMeshRenderer, ParticleRenderer,
WbDrawDispatcher's pre-rewrite path) keep working unchanged.
Required for Task 3's Bindless* methods which need the Texture2DArray
target so the WB modern shader can sample via sampler2DArray. Same
surface may be uploaded both ways during the N.5/N.6 transition;
doubling is bounded and acceptable. After N.6 retires legacy
renderers entirely, the legacy UploadRgba8 becomes unused and is
deleted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementer caught that the original Task 2 (replace UploadRgba8 target
with Texture2DArray) would break four legacy consumers whose shaders
sample via sampler2D: WbDrawDispatcher (pre-rewrite path),
StaticMeshRenderer, InstancedMeshRenderer (legacy escape hatch),
ParticleRenderer.
Revised: Task 2 ADDS a parallel UploadRgba8AsLayer1Array. Existing
UploadRgba8 (Texture2D) stays for legacy callers. Task 3's Bindless*
methods will call the new array path with their own cache dictionaries.
Same surface may be uploaded twice during transition; bounded cost.
N.6 cleanup deletes the legacy path.
Task 3 will be amended at dispatch time to reflect parallel caches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught three related issues:
- _gl field stored but never used (TreatWarningsAsErrors=true would
catch this on a clean build, but better to fix it before it bites)
- GL constructor parameter became unused after dropping _gl
- IsAvailable => true is misleading: TryCreate's out parameter is
the canonical signal, the property carries no information
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TextureCacheBindlessTests.cs file is created in Task 3 (where it
gets meaningful test cases), not Task 1. Removed it from Task 1's
Files list and added an explicit note. Caught during Task 1 code review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Silk.NET.OpenGL.Extensions.ARB 2.23.0 package and a thin
BindlessSupport wrapper exposing GetResidentHandle / MakeNonResident /
HasShaderDrawParameters. TryCreate returns false if the bindless
extension isn't present, letting WbFoundationFlag fall back to legacy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brainstormed 2026-05-08 over 8 design questions. Captures:
- Texture model: sampler2DArray for ALL textures (1-layer wrap for
per-instance composites). Matches WB's modern shader, future-proofs
for atlas adoption in N.6+.
- Translucency: WB's two-pass alpha-test (no native Additive on GfxObj
surfaces; falsifiable at visual verification).
- Data delivery: all-SSBO. Instances[] at binding=0, Batches[] at
binding=1. Indexed by gl_BaseInstanceARB+gl_InstanceID and
gl_DrawIDARB respectively.
- Bindless residency: resident on upload, never release. Bounded
content; instrument under ACDREAM_WB_DIAG=1.
- Escape hatch: two-way flag preserved. N.5 replaces N.4's draw method
in place; legacy InstancedMeshRenderer remains the safety net.
- Perf measurement: CPU stopwatch + GL_TIME_ELAPSED queries, logged
via [WB-DIAG]. Acceptance gates pasted into SHIP commit.
- Persistent-mapped buffers: deferred to N.6.
- Per-instance highlight (selection blink): deferred; field reserved
in InstanceData for Phase B.4 follow-up.
Spec at docs/superpowers/specs/2026-05-08-phase-n5-modern-rendering-design.md
covers architecture, components, per-frame data flow walk-through,
translucent rendering, error handling + fallback, testing + acceptance,
risks, and explicit out-of-scope list. Plan + task breakdown comes next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detailed briefing for the next agent picking up Phase N.5 (Modern
Rendering Path: bindless textures + glMultiDrawElementsIndirect on
N.4's foundation). Covers:
- Where N.4 left things (commits, what works, gotchas inherited)
- The two-feature pairing (why bindless + indirect together)
- Files to read first (WB shaders, our dispatcher, CLAUDE.md cribs)
- 8 brainstorm questions to resolve before spec
- Spec + plan structure (matching N.4's pattern)
- Acceptance criteria
- Things to explicitly NOT do
Sized for a fresh session to pick up cold without spelunking through
months of session history.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase N.4 (Rendering Pipeline Foundation) ships. WbFoundationFlag
flips to default-on (== "1" → != "0"). WB's ObjectMeshManager is
now acdream's production mesh pipeline; WbDrawDispatcher is the
production draw path. Legacy InstancedMeshRenderer is retained as
ACDREAM_USE_WB_FOUNDATION=0 escape hatch until N.6 retires it.
Visual verification at Holtburg passed:
- Scenery (trees / rocks / fences / buildings) renders correctly
- Characters connected with full close-detail geometry (Issue #47
preserved — GfxObjDegradeResolver path intact)
- FPS substantially improved by grouped instanced draws + per-entity
AABB cull + opaque front-to-back sort + palette-hash memoization
Three high-value WB API gotchas surfaced during Task 26 visual
verification and are now documented in CLAUDE.md "WB integration
cribs" + plan Adjustments 7-9 + memory project_phase_n4_state.md:
1. ObjectMeshManager.IncrementRefCount only bumps a counter — does
NOT trigger mesh loading. Call PrepareMeshDataAsync explicitly.
2. ObjectRenderBatch.SurfaceId is unset — read batch.Key.SurfaceId.
3. Modern rendering (GL 4.3 + bindless = every modern GPU) packs
every mesh into ONE global VAO/VBO/IBO. Use
glDrawElementsInstancedBaseVertex(BaseInstance) with FirstIndex +
BaseVertex from the batch, not naive DrawElementsInstanced.
Plan doc flipped to Final state. Roadmap N.4 → Live ✓; N.5 rebranded
from "Terrain rendering" to "Modern rendering path" (bindless +
multi-draw indirect on top of N.4's foundation; terrain rendering
moves to N.5b). CLAUDE.md "Currently in flight" pointer updated to
N.5. New memory file project_phase_n4_state.md preserves the three
WB gotchas for cross-session continuity.
n4-verify*.log added to .gitignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four small wins on top of the grouped-instanced refactor.
1. Drop unused animState lookup. Was a side-effect-free
_entitySpawnAdapter.GetState call per per-instance entity, made
redundant by the Issue #47 fix that trusts MeshRefs.
2. Front-to-back sort opaque groups. Squared distance from camera to
each group's first-instance translation; ascending sort. Lets the
GPU's depth test reject fragments behind closer geometry — real
win on dense scenes (Holtburg courtyard, Foundry interior).
3. Per-entity AABB frustum cull. 5m-radius AABB check per entity
before walking parts. Skips work for distant entities even when
their landblock is partially visible. Animated entities (other
characters, NPCs, monsters) bypass — they always need per-frame
work for animation regardless. Conservative radius covers typical
entity bounds; large outliers stay landblock-culled.
4. Memoize palette hash per entity. TextureCache.HashPaletteOverride
is now internal; new GetOrUploadWithPaletteOverride overload takes
a precomputed hash. The dispatcher computes it ONCE per entity and
reuses across every (part, batch) lookup, avoiding the per-batch
FNV-1a fold over SubPalettes. Trees / scenery without palette
overrides skip entirely (palHash stays 0).
Visual output unchanged; FPS up further, especially in dense scenes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs surfaced and resolved during Task 26 visual verification.
1. **No-scenery + exploded characters**: WB's modern rendering path
(GL 4.3 + bindless) packs every mesh into a single global VAO/VBO/IBO
(GlobalMeshBuffer). Each batch references its slice via FirstIndex
(offset into IBO) + BaseVertex (offset into VBO). The dispatcher's
DrawElementsInstanced(indices=0) read offset 0 of the global IBO
for every entity — drawing the same first triangle from every
entity position. Switched to glDrawElementsInstancedBaseVertex(
BaseInstance) with the batch's offsets. Scenery + connected
characters now render correctly.
2. **Issue #47 character regression**: Adjustment 6 stored
AnimPartChanges on WorldEntity.PartOverrides using the raw
server-sent NewModelId (no degrade resolver applied). The
dispatcher's animState.ResolvePartGfxObj override path then
clobbered MeshRefs (which GameWindow's spawn code correctly
resolves to close-detail meshes via GfxObjDegradeResolver).
Result: humanoids drew low-detail (~14 verts/17 polys) base
meshes instead of close-detail (~32 verts/60 polys), losing
bicep / shoulder / back geometry. Fix: trust MeshRefs as the
source of truth and don't re-apply animState overrides at draw
time. AnimatedEntityState's overrides only matter for hot-swap
appearance updates (0xF625) which today rebuild MeshRefs anyway.
3. **Performance — sub-100 FPS on Holtburg**: per-entity
single-instance draws meant ~16K glDraw calls/frame plus a
64-byte glBufferSubData per call. Refactored to grouped
instanced rendering: bucket all (entity, batch) pairs by
GroupKey(Ibo, FirstIndex, BaseVertex, IndexCount, TextureHandle,
Translucency); upload all matrices in ONE BufferData call;
one glDrawElementsInstancedBaseVertexBaseInstance per group
with BaseInstance pointing at the group's slice in the shared
instance VBO. Down from ~16K to a few hundred draws/frame
(~30× fewer). Bind VAO once per frame (modern WB shares one
global VAO). Removed redundant per-draw VertexAttribPointer
(VAO captures that state).
Result: Holtburg renders correctly with characters showing full
detail; FPS climbed substantially. Two more bugs (mesh loading
+ batch.Key.SurfaceId) were fixed in the prior commit (943652d).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Task 26 visual verification surfaced three bugs in the dispatcher.
Two are fixed here; the third is documented as a remaining issue.
1. WB's IncrementRefCount only bumps a usage counter — it does NOT
trigger mesh loading. Fixed in WbMeshAdapter.IncrementRefCount:
call PrepareMeshDataAsync(id, isSetup: false) on first registration.
Result auto-enqueues to _stagedMeshData (line 510 of WB's
ObjectMeshManager) which Tick() drains onto the GPU.
2. EntitySpawnAdapter never registered per-instance entity meshes
with WB. LandblockSpawnAdapter only registers atlas-tier
(ServerGuid == 0); per-instance entities fell through. Fixed by
adding optional IWbMeshAdapter constructor param + tracking unique
GfxObj ids per server-guid for IncrementRefCount on OnCreate /
DecrementRefCount on OnRemove.
3. WbDrawDispatcher.ResolveTexture used batch.SurfaceId which WB
never populates (line 1746 of ObjectMeshManager only sets
batch.Key — the TextureKey struct that has SurfaceId). Switched
to batch.Key.SurfaceId.
Plus diagnostic counters (ACDREAM_WB_DIAG=1) for entity-seen / drawn
/ mesh-missing / draws-issued counts.
Status: with these fixes the dispatcher now issues real draw calls
(~16K/frame, validated via diagnostic). However visual verification
shows characters appear "exploded" (parts spaced too far apart) and
scenery (trees/rocks/fences/buildings) does not appear. Root cause
analysis pending — Adjustment 7 in the plan documents the deferred
work. Flag stays default-off; legacy renderer remains the
production path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SkyRenderer builds its own SkySubMesh structs from GfxObjMesh.Build
at initialization time, with its own VAO/VBO/IBO resources. It reads
Luminosity, Diffuse, NeedsUvRepeat, SurfOpacity, DisableFog from
GfxObjSubMesh directly — not from AcSurfaceMetadataTable. The sky
draw path never touches InstancedMeshRenderer or WbDrawDispatcher.
No code changes needed: the flag has zero effect on sky rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WbDrawDispatcher draws all entities through WB's ObjectRenderData
(VAO/VBO per GfxObj, per-batch IBO) using acdream's TextureCache for
texture resolution. Two-pass rendering (opaque+ClipMap, then
translucent) matching the existing InstancedMeshRenderer pattern.
Per-entity single-instance drawing for N.4 simplicity — true
instancing grouping deferred to N.6.
Atlas-tier entities: mesh from WB, texture from TextureCache via
batch SurfaceId. Per-instance-tier entities: AnimatedEntityState
drives part overrides + hidden-parts, palette/surface overrides
resolve through TextureCache's composite-key caches.
Side-table population (Task 23 folded in): WbMeshAdapter now takes
DatCollection and populates AcSurfaceMetadataTable on first
IncrementRefCount per GfxObj. The side-table provides TranslucencyKind
(critical for ClipMap alpha-test on vegetation) plus Luminosity,
Diffuse, SurfOpacity, NeedsUvRepeat, DisableFog for sky-pass and
lighting.
GameWindow wiring: when WbFoundationFlag is enabled, WbDrawDispatcher
draws everything and InstancedMeshRenderer is skipped. Flag-off path
is unchanged.
Matrix composition: restPose * animOverride * entityWorld, matching
the spec. Three MatrixCompositionTests verify the contract.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>