-
Notifications
You must be signed in to change notification settings - Fork 0
docs(adr): ADR 0011 — root data as GPU pointer, thin PSOs, split barriers (#691 Phase 6) #761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,19 @@ constrain each other: the resource/binding model (§1), backend selection (§2), | |
| and the PSO-cache + hot-reload story (§3). They are kept in separate sections so | ||
| a future revisit can target one without reopening the others. | ||
|
|
||
| **§4, §5, and §6 were added later (2026-08-08, ahead of Phase 6), not part of | ||
| the original Phase 1 output.** They decide the root-data model (§4, including | ||
| its GPU-driven-indirect consequence in §4.2), PSO permutation minimization | ||
| (§5), and split-barrier/timeline-semaphore signaling (§6) the same way §1–§3 | ||
| decided Phase 2/3's shape before those phases started — pre-deciding a later | ||
| phase from this ADR is the established pattern, not a new one. All three were | ||
| prompted by cross-referencing Sebastian Aaltonen's *Reducing Graphics API | ||
| Complexity* (2026) against this document. §4/§4.2 close a gap: §1.2 already | ||
| assumed heap-bindless *texture* binding, but left buffer binding (UBO/SSBO) | ||
| and indirect-args staging on the conventional, CPU-round-tripping path. §6 | ||
| closes a different gap: §1.5 designed same-submission barriers but never | ||
| addressed cross-pass/cross-frame latency hiding. | ||
|
|
||
| **No code motion.** This ADR ships alongside declaration-only headers under | ||
| `OloEngine/src/OloEngine/Renderer/RHI/` (the vocabulary, no implementations) and | ||
| a coverage ratchet (`OloEngine/tests/Rendering/RHIBoundaryRatchetTest.cpp`). | ||
|
|
@@ -737,6 +750,226 @@ follow-up, not a Phase 6 dependency. | |
|
|
||
| --- | ||
|
|
||
| ## 4. Decision — root data is one GPU pointer per draw/dispatch, not per-draw bound buffers | ||
|
|
||
| §1.2 said a persistent view's heap offset "goes into a UBO/SSBO field," which | ||
| quietly assumed conventional buffer *binding* survives for everything that | ||
| isn't a texture. It shouldn't: amendment (5)'s sweep found "buffer binding | ||
| points (`glBindBufferBase`)" was **the single biggest gap category** — 26 call | ||
| sites, UBO and SSBO — and a Vulkan backend that lowers each of those to its own | ||
| `vkCmdBindDescriptorSets`-shaped call per draw reopens, for buffers, exactly | ||
| the per-draw CPU binding cost ADR 0010 rejected for textures. The heap made | ||
| texture binding free; nothing in the model so far makes buffer binding free. | ||
|
|
||
| **Decision: every draw/dispatch's per-object data — transforms, material | ||
| scalars, texture-heap base indices, and pointers to vertex/index/other GPU | ||
| buffers — is packed into one POD struct, allocated from a per-command-buffer | ||
| GPU-visible bump allocator, and reaches the shader as a single 64-bit pointer. | ||
| No other binding call happens per draw.** This is the direct engine-side | ||
| adoption of the "root data = one pointer" model Sebastian Aaltonen describes in | ||
| *Reducing Graphics API Complexity* (2026) — the outside corroboration is | ||
| recorded here on the same basis §1.9 records Philip Rebohle's account: it | ||
| independently reaches the same shape ADR 0010's heap-bindless commitment was | ||
| already pointed at, and the shader-side dependency it needs | ||
| (`VK_KHR_shader_untyped_pointers`) was *already* pinned in ADR 0010's | ||
| capability contract — for the narrower reason of heap-offset dereferencing. | ||
| Widening its use to carry the whole root struct is a design decision, not a | ||
| new dependency. | ||
|
|
||
| **Buffers become pointers, not heap slots.** Vertex, index, and storage data | ||
| are addressed by buffer device address embedded directly in the root struct, | ||
| mirroring how a texture is addressed via `HeapOffset` (§1.1). The resource heap | ||
| therefore continues to hold only texture descriptors, exactly as §1.2a already | ||
| decided for the unrelated reason that texture and buffer descriptors aren't | ||
| the same size — no analogous "buffer heap" is ever built, and the two | ||
| decisions reinforce rather than duplicate each other. | ||
|
|
||
| **Per-frame/per-view globals do not get a special case.** Camera and lighting | ||
| data change once per frame, not per draw, so binding them the old way is not | ||
| where the 26-site cost lives. They are threaded through the model anyway — as | ||
| a pointer field on the root struct, or on whatever root struct a pass's own | ||
| per-draw structs chain to — so there is exactly **one** binding operation per | ||
| draw/dispatch (pushing the root pointer) with no second, "important enough to | ||
| bind conventionally" tier. Uniformity is the point: a special case here is | ||
| where the next 26-site regrowth would start. | ||
|
|
||
| **The Vulkan-specific wrinkle the talk's slides gloss over: push constants are | ||
| small.** The guaranteed minimum is 128 bytes (desktop drivers on the ADR 0010 | ||
| floor typically expose 256), which is nowhere near enough to hold a root | ||
| struct with several matrices and pointers inline. The push constant therefore | ||
| carries **only the 8-byte GPU pointer** to the bump-allocated struct — never | ||
| the struct's fields directly. This matches the talk's own CUDA-kernel-launch | ||
| framing (the "argument" is the pointer, not the payload) but is worth stating | ||
| explicitly because a first implementation reaching for `vkCmdPushConstants` | ||
| with an inline struct will silently truncate past 128/256 bytes rather than | ||
| fail loudly. | ||
|
Comment on lines
+796
to
+805
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: The Vulkan specification does not allow for silent truncation of push constant data when calling vkCmdPushConstants [1][2]. The size parameter provided to vkCmdPushConstants is strictly validated against the device's physical limits, and any call that exceeds the allowed range constitutes a validation error [1][2]. Specifically, the Vulkan specification imposes the following validation requirements for the size parameter: 1. The sum of the offset and size parameters must be less than or equal to the device's maxPushConstantsSize limit (VUID-vkCmdPushConstants-size-00371) [1][2]. 2. The size must be a multiple of 4 bytes (VUID-vkCmdPushConstants-size-00369) [1][2]. 3. The size must be greater than 0 (VUID-vkCmdPushConstants-size-arraylength) [1][2]. Because these are explicitly defined as validation requirements, if an application passes a size that exceeds maxPushConstantsSize, it violates the specification [1][2]. Under the Vulkan validation layers, such a call will trigger a validation error, and behavior is undefined if the application continues execution despite this error. There is no mechanism in the specification that silently truncates the data to fit within the limit; instead, the API call itself is considered invalid [1][2]. Application developers are responsible for ensuring that their push constant updates do not exceed the maxPushConstantsSize limit reported by the physical device [3][4]. Citations:
Correct the push-constant failure description.
🤖 Prompt for AI Agents |
||
|
|
||
| **The GPU temp/bump allocator does not exist yet and is new work, not a | ||
| reuse of `TransientPool`.** `TransientPool` (§1.2) allocates and aliases | ||
| *physical resources* (textures, buffers as objects); this allocator hands out | ||
| byte ranges *within* a resource for CPU-written, GPU-read scratch data — closer | ||
| to Vulkan's per-command-buffer push-data pattern or Metal's `setBytes` than to | ||
| anything in the render graph today. Backed by VMA (already vendored, Phase 4) | ||
| with a linear/ring strategy, reset at the same frame boundary | ||
| `TransientPool::ReleaseAll()` already uses, so the two lifetimes stay aligned | ||
| without inventing a third one (§1.2's "do not invent a third lifetime class" | ||
|
Comment on lines
+813
to
+815
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)(0011-rhi-neutral-resource-and-binding-model\.md|.*rhi.*|.*RHI.*|.*fence.*|.*TransientPool.*)$|docs/adr/0011-rhi-neutral-resource-and-binding-model.md' || true
echo "== target lines =="
if [ -f docs/adr/0011-rhi-neutral-resource-and-binding-model.md ]; then
wc -l docs/adr/0011-rhi-neutral-resource-and-binding-model.md
sed -n '780,830p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md
echo "== relevant cross-frame lines =="
sed -n '930,970p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md
fi
echo "== search allocation/fence/TransientPool mentions =="
rg -n "TransientPool::ReleaseAll|ReleaseAll|RHI::GpuFence|GpuFence|root data|map|mapped|timeline|retire|Retire|fence complete|command buffer" docs/adr/0011-rhi-neutral-resource-and-binding-model.md .Repository: drsnuggles8/OloEngineBase Length of output: 50382 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== ADR target sections =="
if [ -f docs/adr/0011-rhi-neutral-resource-and-binding-model.md ]; then
echo "target 780-825:"
sed -n '780,825p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md
echo "fence section 930-970:"
sed -n '930,970p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md
echo "lifecycle/fence/timeline mentions in ADR:"
rg -n "TransientPool::ReleaseAll|RHI::GpuFence|GpuFence|timeline|retire|Retire|submitted|GPU|sync|mapped|map|lifetime|frame boundary|cross-frame|Reset|Release" docs/adr/0011-rhi-neutral-resource-and-binding-model.md --max-count 80
fi
echo "== exact allocation/fence references in repo (filtered) =="
rg -n "TransientPool::ReleaseAll|RHI::GpuFence|GpuFence|timeline|Retire|retire|RootData|mapped|ReleaseAll" . \
--glob '!*.png' --glob '!*.jpg' --glob '!*.jpeg' --glob '!*.gif' \
--glob '!*.svg' --glob '!*.tiff' --glob '!*.otf' --glob '!*.ttf' \
--glob '!*.woff' --glob '!*.woff2' --glob '!*.eot' --glob '!*.map' \
--max-count 120Repository: drsnuggles8/OloEngineBase Length of output: 50382 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
path = Path("docs/adr/0011-rhi-neutral-resource-and-binding-model.md")
text = path.read_text()
start = text.index("reset at the same frame boundary")
end = text.index("This is the one part of", start)
proposal = text[start:end].strip()
checks = {
"resets_at_TransientPool_ReleaseAll_boundary": "TransientPool::ReleaseAll()" in proposal,
"persistently_mapped": "persistently mapped" in proposal.lower(),
"backed_by_VMA": "VMA" in proposal and "Vulkan Memory Allocator" not in proposal,
"cpu_writes_directly": "CPU writes" in proposal and "into GPU memory" in proposal,
"timeline_semaphore_present": re.search(r"\b**timeline semaphore**", text) is not None,
"gpu_fence_uses_same_lifetime": "same per-frame lifetime discipline" in text and "RHI::GpuFence" in text,
}
for k, v in checks.items():
print(f"{k}={v}")
print("\nRelevant source:")
print(proposal)
print("\nFence allocation snippet:")
print(text[text.index("RHI::GpuFence"):text.index("one persistently-mapped counter", text.index("RHI::GpuFence")) + len("one persistently-mapped counter")])
PYRepository: drsnuggles8/OloEngineBase Length of output: 952 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
path = Path("docs/adr/0011-rhi-neutral-resource-and-binding-model.md")
text = path.read_text()
start = text.index("reset at the same frame boundary")
end = text.index("This is the one part of", start)
proposal = text[start:end].strip()
checks = {
"resets_at_TransientPool_ReleaseAll_boundary": "TransientPool::ReleaseAll()" in proposal,
"persistently_mapped": "persistently mapped" in proposal.lower(),
"backed_by_VMA": "VMA" in proposal and "Vulkan Memory Allocator" not in proposal,
"cpu_writes_directly": "CPU writes" in proposal and "into GPU memory" in proposal,
"timeline_semaphore_present": "**timeline semaphore**" in text,
"gpu_fence_uses_same_lifetime": "same per-frame lifetime discipline" in text and "RHI::GpuFence" in text,
"proposal_mentions_retirement_or_fence_completed": bool(re.search(r"(retir|fence complete|timeline completed)", proposal, flags=re.I)),
}
for k, v in checks.items():
print(f"{k}={v}")
print("\nRelevant source:")
print(proposal)
print("\nFence allocation snippet:")
print(text[text.index("RHI::GpuFence"):text.index("one persistently-mapped counter", text.index("RHI::GpuFence")) + len("one persistently-mapped counter")])
PYRepository: drsnuggles8/OloEngineBase Length of output: 944 Tie allocator reset to GPU retirement.
🤖 Prompt for AI Agents |
||
| rule applies here too). Because the allocation is persistently mapped | ||
| (ReBAR/UMA, per the driver floor), the CPU writes the struct's fields directly | ||
| into GPU memory — no staging buffer, no copy command. This is the one part of | ||
|
Comment on lines
+816
to
+818
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and inspect the relevant ADR section, plus nearby allocation/mapping mentions.
file="docs/adr/0011-rhi-neutral-resource-and-binding-model.md"
if [ -f "$file" ]; then
echo "== file exists =="
wc -l "$file"
echo
echo "== relevant lines 780-845 =="
sed -n '780,845p' "$file" | nl -ba -v780
else
echo "missing $file"
fi
echo
echo "== allocate/map/host coherency mentions in ADR =="
rg -n -i 'persist.map|persistently mapped|VMA|vma|vk_allocate_memory|deviceLocal|hostVisible|HOST_COHERENT|memory property|mapping|map memory|flush|invalidate' "$file"Repository: drsnuggles8/OloEngineBase Length of output: 313 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="docs/adr/0011-rhi-neutral-resource-and-binding-model.md"
echo "== relevant lines 780-845 =="
sed -n '780,845p' "$file"
echo
echo "== allocation/map/host coherency mentions in ADR =="
rg -n -i 'persist.map|persistently mapped|VMA|vma|vk_allocate_memory|deviceLocal|hostVisible|HOST_COHERENT|memory property|mapping|map memory|flush|invalidate' "$file"Repository: drsnuggles8/OloEngineBase Length of output: 6847 🌐 Web query:
💡 Result: When working with Vulkan memory that is host-visible but lacks the VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, the memory is considered non-coherent [1][2]. In this state, the host (CPU) and device (GPU) may maintain separate, inconsistent views of the memory in their respective caches [3][4]. To manage visibility and availability of data in non-coherent memory, you must manually manage cache consistency using the following mechanisms: 1. CPU Cache Management: Before the device reads data written by the CPU, you must call vkFlushMappedMemoryRanges to flush the CPU's writes out to the memory domain [5][4]. Conversely, before the CPU reads data written by the device, you must call vkInvalidateMappedMemoryRanges to discard any stale CPU cache lines and pull in the latest data from the memory domain [6][3]. 2. Alignment Constraints: When working with non-coherent memory, operations must be performed on memory ranges aligned to the VkPhysicalDeviceLimits::nonCoherentAtomSize [5][4]. If you do not explicitly flush or invalidate on these boundaries, you may encounter data inconsistencies or race conditions [5][4]. 3. Execution Synchronization: Even if memory is coherent, it does not imply synchronization of execution [3]. You must always use proper Vulkan synchronization primitives (such as barriers, fences, or semaphores) to ensure that the producer has finished writing and the consumer is ready to read [3][5][7]. Coherence only handles the data movement between caches; it does not replace the need to order operations relative to one another [3]. In summary, for memory that is Device Local and Host Visible but not Host Coherent, you are responsible for both explicit cache management (flushing/invalidating on the CPU) and explicit execution synchronization (to prevent data races) [3][5][4]. If a memory type lacks the host coherent property, the implementation will not automatically make writes visible across the CPU/GPU boundary [1][3]. Citations:
🌐 Web query:
💡 Result: In Vulkan, memory coherence and visibility are governed by specific memory property flags and synchronization requirements [1][2]. Here is the breakdown of how these concepts interact with host-mapped memory and device-local memory. Memory Property Flags VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT: This flag indicates that the memory can be mapped into the host's address space using vkMapMemory, allowing the CPU to read or write to it directly [1][3]. VK_MEMORY_PROPERTY_HOST_COHERENT_BIT: When this flag is set, host cache management (vkFlushMappedMemoryRanges and vkInvalidateMappedMemoryRanges) is not required [1][2]. The hardware automatically ensures that CPU writes to the mapped memory are available to the device, and device writes are visible to the CPU [1][4]. Host-Visible, Device-Local Memory It is possible to allocate memory that is both host-visible and device-local [5][6]. This allows the CPU to access the memory directly while the GPU enjoys high-performance access as if it were VRAM [6][7]. When this memory also has the HOST_COHERENT_BIT set, the CPU and GPU can interact without explicit cache flushing [6][7]. Device Writes and CPU Mapping Regardless of coherence, you must guarantee synchronization when the device and host share access to memory [3]. If the memory is host-coherent, you do not need to call vkFlushMappedMemoryRanges or vkInvalidateMappedMemoryRanges, but you must still provide proper execution dependencies (e.g., using pipeline barriers or semaphores) to ensure that the device has finished writing to or reading from the memory before the CPU accesses it, and vice versa [3][7]. If the memory is NOT host-coherent, you must manually manage cache visibility using vkFlushMappedMemoryRanges and vkInvalidateMappedMemoryRanges [1][3]. Furthermore, when mapping such memory, you are responsible for flushing or invalidating the memory in increments of at least VkPhysicalDeviceLimits::nonCoherentAtomSize to ensure data integrity [3][8]. Summary Table Memory Property | Host Cache Management (Flush/Invalidate) Required? | Notes --- | --- | --- Host Coherent | No [1][7] | Still requires execution synchronization (barriers/semaphores) [3][7]. Non-Coherent | Yes [1][3] | Must manage flush/invalidate ranges (aligned to nonCoherentAtomSize) [3][8]. For optimal performance, applications typically use device-local memory for steady-state data and host-visible/coherent memory for dynamic updates or staging [6][7]. Citations:
Define host visibility for mapped root writes. This allocation is host-visible, but persistent mapped memory is not host-coherent by default. If the Vulkan/VMA allocation lacks 🤖 Prompt for AI Agents |
||
| the model that is *easier* than the texture-upload path (§1's texture-upload | ||
| discussion), not harder: root data has no tiling or compression to preserve. | ||
|
|
||
| ### 4.1 What this replaces, and what stays | ||
|
|
||
| The two new virtuals amendment (5) added for `glBindBufferBase` (the "buffer | ||
| binding points" row, 2 virtuals) stay in the facade **for GL**, unchanged — | ||
| this is a Vulkan-side simplification, not a neutral-layer one, following the | ||
| same pattern §1.6 used for `Renderer/Debug/`: a backend-specific improvement | ||
| does not have to be expressed as a shared abstraction change. The Vulkan | ||
| backend simply never calls them; per-draw data reaches its shaders exclusively | ||
| through the root-pointer path above. | ||
|
|
||
| **Owner: Phase 6**, and it should land *before* Phase 6's "one already-golden- | ||
| tested pass renders correctly" checkpoint — a pass converted to bound-UBO | ||
| Vulkan first and to root-data-pointer Vulkan second is strictly more work than | ||
| doing it once, and the checkpoint pass is what every later pass in Phase 7 | ||
| will be copied from. | ||
|
|
||
| ### 4.2 GPU-driven indirect root data: no separate shape for a compute-written draw | ||
|
|
||
| Because root data is just a GPU pointer, the struct an ordinary draw call | ||
| points to and the struct an *indirect* draw call points to are the same kind | ||
| of thing — the only difference is who writes it. **Decision: indirect | ||
| draw/dispatch takes the identical single-pointer root-data contract as a | ||
| direct call.** There is no separate "indirect root data" shape, no CPU-side | ||
| indirect-args staging step, and no driver-managed command-signature object | ||
| (the DX12-shaped `ID3D12CommandSignature` has nothing to abstract here, | ||
| because the pointer *is* the argument list already). A GPU-driven pass — | ||
| visibility/occlusion culling, LOD selection, virtual-geometry cluster culling | ||
| (`VirtualClusterCull.comp`, per `docs/agent-rules/cluster-lod-simplification.md`) | ||
| — writes its output root-data structs directly into GPU memory and issues the | ||
| indirect draw pointing at them; nothing round-trips through the CPU. This | ||
| codebase already has prior art for "a GPU-written buffer is the argument list," | ||
| in the two-counter overflow contract issue #725's GPU debug draws use — the | ||
| same shape, applied here to indirect draw/dispatch arguments instead of debug | ||
| primitives. | ||
|
Comment on lines
+838
to
+855
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Locate ADR and related files =="
fd -a '0011-rhi-neutral-resource-and-binding-model|cluster-lod-simplification|RenderGraph|BarrierPlan|Indirect' . | sed 's#^\./##' | head -200
echo
echo "== Inspect relevant ADR lines =="
if [ -f docs/adr/0011-rhi-neutral-resource-and-binding-model.md ]; then
sed -n '800,870p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md | cat -n
fi
echo
echo "== Search for indirect draw/dispatch/vk indirect definitions and root data terms =="
rg -n "indirect|DrawIndirect|DispatchIndirect|root data|root pointer|RootData|drawId|firstInstance|draw index|pipeline layout|descriptor" docs src include apps test -S 2>/dev/null | head -300Repository: drsnuggles8/OloEngineBase Length of output: 30606 🌐 Web query:
💡 Result: In Vulkan, the VkDrawIndirectCommand and VkDispatchIndirectCommand structures are used to provide parameters for indirect drawing and compute dispatching commands, respectively. Both structures are composed entirely of 32-bit unsigned integers [1][2]. VkDrawIndirectCommand Structure The VkDrawIndirectCommand structure specifies the parameters for an indirect drawing command [1]: typedef struct VkDrawIndirectCommand { uint32_t vertexCount; uint32_t instanceCount; uint32_t firstVertex; uint32_t firstInstance; } VkDrawIndirectCommand; Fields: vertexCount: The number of vertices to draw [1][3]. instanceCount: The number of instances to draw [1][3]. firstVertex: The index of the first vertex to draw [1][3]. firstInstance: The instance ID of the first instance to draw [1][3]. The members of this structure have the same meaning as the parameters of the vkCmdDraw command [1][4]. VkDispatchIndirectCommand Structure The VkDispatchIndirectCommand structure specifies the parameters for an indirect dispatching command [2]: typedef struct VkDispatchIndirectCommand { uint32_t x; uint32_t y; uint32_t z; } VkDispatchIndirectCommand; Fields: x: The number of local workgroups to dispatch in the X dimension [2][5]. y: The number of local workgroups to dispatch in the Y dimension [2][5]. z: The number of local workgroups to dispatch in the Z dimension [2][5]. The members of this structure have the same meaning as the corresponding parameters of the vkCmdDispatch command [2][5]. These values must be less than or equal to the corresponding limits defined in VkPhysicalDeviceLimits::maxComputeWorkGroupCount [2][5]. Citations:
Define how indirect commands select root data.
State whether one pushed pointer addresses an array of root records, how 🤖 Prompt for AI Agents |
||
|
|
||
| The only new requirement is the ordinary one: a barrier between the writing | ||
| compute dispatch and the consuming indirect draw, using §1.5's existing | ||
| hazard-flag model (an indirect-args hazard, the same category §1.5 already | ||
| names for descriptor writes). No new barrier machinery — this is an | ||
| application of §1.5, not an extension of it. | ||
|
Comment on lines
+857
to
+861
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Repository files around ADR 0011:"
if [ -f docs/adr/0011-rhi-neutral-resource-and-binding-model.md ]; then
wc -l docs/adr/0011-rhi-neutral-resource-and-binding-model.md
sed -n '820,880p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md | cat -n
else
echo "doc file missing"
fi
echo
echo "Search for RenderGraph::ComputeBarrierPlan and hazard terminology:"
rg -n "RenderGraph::ComputeBarrierPlan|ComputeBarrierPlan|indirect-args|indirect args|indirect[ -]arg|root-data|root_data|hazard|barrier" docs src . --glob '!*' 2>/dev/null | head -n 200Repository: drsnuggles8/OloEngineBase Length of output: 4213 Register the root-data buffer hazard too. §4.2 says a GPU-driven pass writes root-data structs directly and issues the indirect draw. If 🤖 Prompt for AI Agents |
||
|
|
||
| **The failure mode worth naming for Phase 7:** a converted GPU-driven pass | ||
| that still stages its indirect args through the CPU is not incorrect, but it | ||
| is leaving §4's investment on the table — the same category of regression as | ||
| converting a pass to bindless textures while leaving one sampler on the slot | ||
| path (Phase 3's finding, ADR amendments (32)-(38)). Treat a CPU-staged | ||
| indirect-args path in a newly-ported GPU-driven pass as a defect to fix, not | ||
| a style choice, once §4 exists to make the alternative free. | ||
|
|
||
| **Owner:** the mechanism is established in Phase 6 alongside the rest of §4; | ||
| individual GPU-driven passes (culling, LOD, virtual geometry) adopt it | ||
| pass-by-pass as Phase 7 ports them. | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Decision — minimize PSO permutation axes before Phase 6's first `VkPipeline` | ||
|
|
||
| §3 designs the PSO *cache* — invalidation, hot reload, the shader→pipeline | ||
| reverse index — but not what varies a `VkPipeline` in the first place. Left | ||
| undecided, Phase 6 will bake whatever is convenient into | ||
| `VkGraphicsPipelineCreateInfo` (the GL-shaped default: vertex layout, depth, | ||
| blend, and raster state all monolithic), and Phase 7's 35+-pass port | ||
| multiplies that decision by every state permutation each pass happens to use. | ||
| This is the same permutation-explosion mechanism ADR 0010/0011 already | ||
| diagnose as the cause of DX12/Vulkan's real-world PSO-hitch problems — the | ||
| difference is that this codebase gets to decide it *before* the first | ||
| pipeline exists, not retrofit it after 35 passes have already baked their own | ||
| assumptions in. | ||
|
|
||
| **Decision, per state axis, following Aaltonen's thin-PSO argument:** | ||
|
|
||
| | Axis | Decision | Why it's safe on this ADR's driver floor | | ||
| | --- | --- | --- | | ||
| | Vertex input layout | **Not baked at all.** Extend Phase 3's bindless-texture pulling pattern to vertex data: vertex shaders fetch through a buffer-device-address pointer with manual indexing, not `VkPipelineVertexInputStateCreateInfo`. | Removes the axis entirely rather than making it cheaper — the same move §4 makes for buffers generally, applied to the one buffer type (vertex) that still has a dedicated binding mechanism. | | ||
| | Depth/stencil state | Dynamic, via `VK_EXT_extended_dynamic_state` / `…state2` (core-promoted). | Universal on desktop; Metal has proven the same split safe across Nvidia/AMD/Intel/Apple since its first version (talk, §4.8). | | ||
| | Blend state | Dynamic where `VK_EXT_extended_dynamic_state3`'s blend-enable/equation states are available; baked into the PSO only where they aren't. | The talk's finding is that *only* Apple/PowerVR mobile GPUs genuinely require blend baked into the shader — irrelevant to this ADR's Vulkan desktop floor (NVIDIA/AMD), so the fallback path should never actually trigger here. Verify at Phase 6's device-capability audit rather than assume — same discipline ADR 0010 used for the heap extension's feature bits. | | ||
| | Rasterizer/render-target description | Stays minimal: target formats/count, depth/stencil format, sample count, dual-source blending flag. Nothing else. | This is already the shape §1's survey implies; stating it here makes it a decision instead of an accident. | | ||
|
Comment on lines
+891
to
+898
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## repo files matching ADR 0011"
git ls-files | rg '(^|/)0011-rhi-neutral-resource-and-binding-model\.md$|ads/adr/0011|docs/adr/0011' || true
echo "## file size"
wc -l docs/adr/0011-rhi-neutral-resource-and-binding-model.md
echo "## relevant lines 860-920"
sed -n '860,920p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md | cat -n
echo "## search for dynamic state and PSO keys"
rg -n "ExtendedDynamicState|extended_dynamic_state|pipeline binding|pipeline key|pipelineKeys|pipeline key|cull|front face|primitive topology|polygon|sample mask|alpha-to-coverage|blend-enable|blend equation" docs/adr/0011-rhi-neutral-resource-and-binding-model.mdRepository: drsnuggles8/OloEngineBase Length of output: 6517 🌐 Web query:
💡 Result: The extension VK_EXT_extended_dynamic_state3 is a Vulkan device extension that allows applications to specify various pipeline states dynamically, reducing the need for creating and binding a large number of monolithic pipelines [1][2]. The states you mentioned are part of the functionality added by this extension [2][3]. Below is a breakdown of these specific dynamic states: VK_DYNAMIC_STATE_COLOR_BLEND_ENABLE_EXT This allows the blendEnable state (defined per attachment in VkPipelineColorBlendAttachmentState) to be set dynamically using vkCmdSetColorBlendEnableEXT [4][5]. VK_DYNAMIC_STATE_COLOR_BLEND_EQUATION_EXT This enables dynamic control over blending equations (specifically srcColorBlendFactor, dstColorBlendFactor, colorBlendOp, srcAlphaBlendFactor, dstAlphaBlendFactor, and alphaBlendOp) for each color attachment using vkCmdSetColorBlendEquationEXT [4][6]. VK_DYNAMIC_STATE_ALPHA_TO_COVERAGE_ENABLE_EXT This allows the alphaToCoverageEnable state (originally from VkPipelineMultisampleStateCreateInfo) to be set dynamically using vkCmdSetAlphaToCoverageEnableEXT [7][8]. VK_DYNAMIC_STATE_SAMPLE_MASK_EXT This permits the sample mask to be set dynamically using vkCmdSetSampleMaskEXT instead of being baked into the pipeline via VkPipelineMultisampleStateCreateInfo::pSampleMask [7][9]. VK_DYNAMIC_STATE_EXCLUSIVE_SCISSOR_ENABLE_NV This state is associated with the VK_NV_scissor_exclusive extension and allows toggling the exclusive scissor test dynamically if supported by the implementation [2][7]. To use these, an application must enable the VK_EXT_extended_dynamic_state3 extension and ensure the corresponding feature bits in the VkPhysicalDeviceExtendedDynamicState3FeaturesEXT structure are supported and enabled (e.g., extendedDynamicState3ColorBlendEnable, extendedDynamicState3SampleMask, etc.) [3][10][6]. When these states are enabled, the corresponding values in the pipeline state objects are ignored at draw time, and the command buffer must have the state explicitly set [5][8][9]. Citations:
🌐 Web query:
💡 Result: The Vulkan dynamic states you listed are part of various "extended dynamic state" extensions designed to reduce the number of Pipeline State Objects (PSOs) an application must compile and bind [1][2]. They are not all part of the same extension; they are distributed across Citations:
🌐 Web query:
💡 Result: The Vulkan extension VK_EXT_extended_dynamic_state2 adds several dynamic states to the graphics pipeline, allowing applications to modify specific states without rebinding or re-creating pipeline objects [1][2]. Key aspects of this extension include: Promotion to Core: Several features originally introduced in VK_EXT_extended_dynamic_state2 were promoted to core functionality in Vulkan 1.3 [1][3]. The following states were promoted, with the EXT suffix removed in the core API [1][4]: - VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE (via vkCmdSetDepthBiasEnable) [1][2] - VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE (via vkCmdSetPrimitiveRestartEnable) [1][2] - VK_DYNAMIC_STATE_RASTERIZER_DISCARD_ENABLE (via vkCmdSetRasterizerDiscardEnable) [1][2] Non-Promoted Features: The following states and their corresponding commands remain as extension-specific functionality and were not promoted to core Vulkan 1.3 [1][3]: - dynamicStateLogicOpEXT (via vkCmdSetLogicOpEXT; uses VK_DYNAMIC_STATE_LOGIC_OP_EXT) [1][5][6] - dynamicStatePatchControlPointsEXT (via vkCmdSetPatchControlPointsEXT; uses VK_DYNAMIC_STATE_PATCH_CONTROL_POINTS_EXT) [1][5][6] Feature Requirements: To use these features, the application must enable them via the VkPhysicalDeviceExtendedDynamicState2FeaturesEXT structure when creating a device [2][5]. This structure contains the following boolean fields [5][7]: - extendedDynamicState2: Enables the base dynamic states (Depth Bias, Primitive Restart, and Rasterizer Discard) [5]. - extendedDynamicState2LogicOp: Enables support for dynamic logic operations [5]. - extendedDynamicState2PatchControlPoints: Enables support for dynamic patch control points [5]. Note on Primitive Topology: While your query mentions dynamicStatePrimitiveTopologyEXT, this specific state is not part of VK_EXT_extended_dynamic_state2. Dynamic primitive topology is typically handled by other extensions, such as VK_EXT_primitive_topology_list_restart or features available in later Vulkan versions and other extensions like VK_EXT_shader_object [8]. Citations:
🌐 Web query:
💡 Result: In Vulkan, these dynamic state constants allow applications to modify specific pipeline states during command buffer recording rather than baking them into monolithic pipeline objects [1][2]. This flexibility helps reduce the number of pipeline state objects (PSOs) that need to be created and compiled [1][3]. The specified dynamic states fall into different categories and extension support levels: 1. Core Vulkan 1.3 States: The following states were originally introduced via the VK_EXT_extended_dynamic_state extension and were promoted to core functionality in Vulkan 1.3 (with the _EXT suffix omitted in core) [4][3]: - VK_DYNAMIC_STATE_CULL_MODE: Allows dynamic setting of the cull mode via vkCmdSetCullMode [4][5]. - VK_DYNAMIC_STATE_FRONT_FACE: Allows dynamic setting of the front-facing triangle orientation via vkCmdSetFrontFace [4][6]. - VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY: Allows dynamic setting of the input assembly primitive topology via vkCmdSetPrimitiveTopology [4][7]. 2. Extended Dynamic State 3 (VK_EXT_extended_dynamic_state3): These states are provided by the VK_EXT_extended_dynamic_state3 extension [8]: - VK_DYNAMIC_STATE_LINE_RASTERIZATION_MODE_EXT: Allows dynamic setting of the line rasterization mode via vkCmdSetLineRasterizationModeEXT (requires support for VK_EXT_line_rasterization or similar) [8][9]. - VK_DYNAMIC_STATE_COLOR_WRITE_MASK_EXT: Allows dynamic setting of the color write masks (this is part of the state managed by extended_dynamic_state3) [8]. Usage: To use these states, you must include the corresponding enumeration value in the pDynamicStates array within the VkPipelineDynamicStateCreateInfo structure when creating a graphics pipeline [1][4]. If a state is enabled as dynamic, the corresponding static value defined in the original pipeline creation structure (e.g., VkPipelineRasterizationStateCreateInfo) is ignored, and the state must be set using the associated command (e.g., vkCmdSetCullMode) before any drawing commands that rely on that state [4][5][6][9]. For states provided by extensions like VK_EXT_extended_dynamic_state3, hardware support is optional, and you should check the appropriate feature structures to ensure the device supports the specific dynamic states you intend to use [8][2]. Citations:
🌐 Web query:
💡 Result: In the Vulkan API, the specific functionality you are asking about is spread across multiple "Extended Dynamic State" extensions rather than a single extension named "extended_dynamic_state_depth_bias" [1][2]. The primary extensions that address these dynamic state requirements are VK_EXT_extended_dynamic_state, VK_EXT_extended_dynamic_state2, and later additions in core Vulkan 1.3 [1][2][3]. Key breakdown of these states: Depth Bias and Polygon Offset: The dynamic state for depth bias (often associated with polygon offset) is managed via: - VK_DYNAMIC_STATE_DEPTH_BIAS [4]: Allows setting depth bias factors (constant, clamp, and slope) dynamically [4]. - VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE [4]: Introduced in VK_EXT_extended_dynamic_state2 (and later core Vulkan 1.3), this allows toggling whether depth bias is enabled [5][1][4]. Line Width: - VK_DYNAMIC_STATE_LINE_WIDTH is part of core Vulkan and has been available since version 1.0, not requiring an extension to be used dynamically [4]. Color Blend and Advanced States: - While basic blend state is standard, advanced dynamic state control for blending and other fixed-function states is typically handled by extensions like VK_EXT_extended_dynamic_state3 (which covers many more states not included in the earlier versions) or through specific hardware features [6]. - "Clamp color" is not a standard dynamic state enum; color clamping is generally handled via pipeline configuration or specific platform-dependent features [7][8]. In summary, if you are looking to move static pipeline states to dynamic command buffer calls to reduce pipeline creation overhead, you should use the combination of VK_EXT_extended_dynamic_state, VK_EXT_extended_dynamic_state2, and evaluate VK_EXT_extended_dynamic_state3 if you require further dynamic control [1][2][6]. Core Vulkan 1.3 incorporates most of the functionality from the first two extensions [1][2][3]. Citations:
Define every PSO dynamic-state axis and fallback. The table currently uses only high-level labels, while the remaining static state can still multiply PSOs if Phase 6 does not lock it down. List cull mode, front face, primitive topology, polygon mode, color write mask, sample mask, alpha-to-coverage, depth bias, line width, and any rasterization or blending flags that may bake into a PSO. For each axis, specify the feature gate, fallback behavior when the dynamic feature is absent, and whether it contributes to the pipeline key. 🤖 Prompt for AI Agents |
||
|
|
||
| **Sequencing note:** vertex-pulling is comparable in size to Phase 3's | ||
| bindless-texture rehearsal (a real conversion across every draw call, not a | ||
| config flag), so it should be an explicit Phase 6 sub-step completed before | ||
| the "one pass renders correctly" checkpoint — not deferred to Phase 7, where | ||
| retrofitting it after 35 passes have been ported against a baked vertex-input | ||
| PSO would cost far more than doing it once, up front, on the one pilot pass. | ||
|
Comment on lines
+900
to
+905
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Files matching vertex pull / OpenGLRendererAPI:"
git ls-files | rg '(^docs/adr/0011-rhi-neutral-resource-and-binding-model\.md$|OloEngine/src/Platform/OpenGL/OpenGLRendererAPI\.cpp$|vertex.?pull|drawing-indirect|vertex-input)' || true
echo
echo "ADR lines 880-920:"
sed -n '880,920p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md || true
echo
echo "OpenGLRendererAPI lines 740-770:"
sed -n '740,770p' OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp || true
echo
echo "Search draw/vertex input symbols in OpenGLRendererAPI:"
rg -n "glMultiDrawElements|VkPipelineVertexInputStateCreateInfo|vertex.?input|VertexInput|DrawIndexedIndirect|MultiDraw|Stride|Binding|Index" OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp OloEngine/src -g '*.{cpp,h,hpp,c,hpp}' || trueRepository: drsnuggles8/OloEngineBase Length of output: 50382 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "ADR line count:"
wc -l docs/adr/0011-rhi-neutral-resource-and-binding-model.md
echo
echo "Focused ADR lines around vertex-pulling note:"
sed -n '860,915p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md
echo
echo "Focused OpenGLRendererAPI lines 730-775:"
sed -n '730,775p' OloEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp
echo
echo "Vertex pulling mentions in ADR:"
rg -n -C 2 -i "vertex.?pull|vertex pulling|pulling" docs/adr/0011-rhi-neutral-resource-and-binding-model.md || true
echo
echo "Vertex format/layout/schema mentions:"
rg -n -C 2 -i "vertex.?format|vertex.?binding|vertex.?stride|vertex.?offset|VkVertexInput|PipelineVertex|vertex-input|vertex_input|draw.?vertex|vertex.?selection|vertex.?contracts|shader-visible" docs OloEngine -g '*.{md,cpp,h,hpp,hlsl,glsl,hlslc}' | head -n 240 || trueRepository: drsnuggles8/OloEngineBase Length of output: 34520 Define the vertex-pulling contract before the Phase 6 checkpoint. Vertex pulling currently describes the 🤖 Prompt for AI Agents |
||
|
|
||
| **What is deliberately not pinned here:** the exact | ||
| `VkPhysicalDeviceExtendedDynamicState3PropertiesEXT` / feature bits Phase 6 | ||
| should require. As with ADR 0010's capability contract, writing specific bit | ||
| names into an ADR with no Vulkan device code to validate them against would be | ||
| guessing; Phase 6 fills this in against the real driver floor, the same way | ||
| Phase 4 filled in ADR 0010's table. | ||
|
|
||
| **Owner: Phase 6**, alongside §4 and ahead of the same checkpoint. | ||
|
|
||
| --- | ||
|
|
||
| ## 6. Decision — split barriers are a GPU-pointer signal/wait pair, the same mechanism as timeline semaphores | ||
|
|
||
| §1.5 designs the *same-command-buffer* barrier: a producer/consumer pair | ||
| inside one recorded submission, expressed as `(FromUsage, ToUsage, Range, | ||
| hazard)`. It says nothing about hiding latency across a *longer* span — the | ||
| case DX12's split barriers and Vulkan's events each address today with their | ||
| own persistent, ceremony-heavy driver object, which is a large part of why, | ||
| per Aaltonen's talk, almost nobody actually uses either. | ||
|
|
||
| **Decision: a split barrier is a plain GPU-memory location plus two | ||
| operations — `Signal(pointer, value, op)` and `Wait(pointer, value, | ||
| compareOp)` — with `op`/`compareOp` covering at minimum `{Set/Equal, | ||
| AtomicMax/GreaterEqual}`.** The max/greater-equal pair is what generalizes | ||
| this from a one-shot flag into a monotonically increasing counter, i.e. a | ||
| timeline. This is deliberately not new Vulkan machinery: it is the existing | ||
| **timeline semaphore** primitive (core since Vulkan 1.2, well inside the ADR | ||
| 0010 1.4 floor), promoted to the render graph's one mechanism for any | ||
| cross-pass or cross-frame dependency, rather than inventing a second, | ||
| Vulkan-events-shaped abstraction alongside it the way DX12 keeps both split | ||
| barriers and fences as separate concepts. | ||
|
|
||
| **How this plugs into the existing model.** `RenderGraphBarrierPlanner` | ||
| (§1.5) keeps computing ordinary same-submission dependencies unchanged — nothing | ||
| here replaces that. A dependency that spans further — a compute culling pass | ||
| whose tail latency can be hidden behind independent work later in the same | ||
| frame, or a cross-frame dependency the render graph currently expresses as an | ||
| ad hoc CPU/GPU fence — becomes an `RHI::GpuFence`: one GPU pointer, allocated | ||
| from the same per-frame lifetime discipline §4's temp allocator already | ||
| established, with `Signal` attached to the producing render-graph node and | ||
| `Wait` attached to the consuming one (or to nothing, if the wait is a CPU-side | ||
| frame-pacing check instead). | ||
|
|
||
| **One primitive, both sides of the API.** The same `Signal`/`Wait` pair | ||
| replaces ad hoc CPU-side fence chains too — a single persistently-mapped | ||
| counter, incremented by `Signal`, observed via a CPU `WaitGreaterEqual` — so | ||
| the render graph's existing CPU/GPU frame-pacing mechanism and its future | ||
| GPU-side split-barrier mechanism are the same code, not two. | ||
|
Comment on lines
+927
to
+954
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Find ADR and related files"
git ls-files | rg 'docs/adr/0011|rhi|GpuFence|RenderGraphBarrierPlanner|gpu fence|split barrier' || true
echo
echo "Relevant sections in ADR 0011"
sed -n '880,970p' docs/adr/0011-rhi-neutral-resource-and-binding-model.md
echo
echo "Search for RHI GpuFence / Signal / Wait definitions or mentions"
rg -n "RHI::GpuFence|GpuFence|Signal\\(|Wait\\(|WaitGreaterEqual|split barrier|timeline semaphore|DeviceMemory|mapped|counter" docs/adr -S || trueRepository: drsnuggles8/OloEngineBase Length of output: 11929 🌐 Web query:
💡 Result: In Vulkan, a timeline semaphore is a synchronization primitive that utilizes a monotonically increasing 64-bit unsigned integer as its payload [1][2][3]. Unlike binary semaphores, which have only two states (signaled/unsignaled), timeline semaphores allow for more complex synchronization by tracking progress against specific integer values [2][4][3]. Key aspects regarding the host and payload operations include: Host Wait and Signal: The host can block until a timeline semaphore reaches a specified value using vkWaitSemaphores [1][5][6]. Conversely, the host can advance the timeline semaphore value using vkSignalSemaphore [7][8][3]. Host Query: Applications can perform non-blocking queries of the current payload value of a timeline semaphore at any time using vkGetSemaphoreCounterValue [1][2][8]. Payload and Memory Mapping: The term "payload" refers to the internal data representing the semaphore's current state [3]. While external semaphore mechanisms allow for importing and exporting these payloads (e.g., between different APIs or processes), Vulkan does not provide a mechanism to directly "memory map" a semaphore payload for host access [3]. Instead, host-side interaction is explicitly handled through the formal API commands (vkWaitSemaphores, vkSignalSemaphore, and vkGetSemaphoreCounterValue), which abstract the underlying implementation [1][2][3]. This design replaces many requirements for VkFence objects, as the host can wait for specific points in a GPU's timeline rather than just waiting for an entire command buffer to complete [2][4]. Citations:
Separate timeline semaphores from mapped GPU-memory signaling. Vulkan timeline semaphores are backend Choose one model:
Also define submission boundaries. Vulkan semaphore signal/wait operations occur around submission command buffers; they are not arbitrary commands between render-graph nodes inside one command buffer. 🤖 Prompt for AI Agents |
||
|
|
||
| **What this does not change:** §1.5's ordinary same-command-buffer barrier | ||
| model stays exactly as decided. This is additive, for the specific case where | ||
| a blocking barrier would leave the GPU idle because there is genuine | ||
| independent work available to fill the gap. | ||
|
|
||
| **Deferred to Phase 7:** which specific render-graph passes are worth | ||
| splitting rather than barriered inline is a per-pass profiling decision, not | ||
| one this ADR can make abstractly — §6 only decides that the primitive exists | ||
| and what shape it has. Applying it is opportunistic, not a requirement every | ||
| ported pass must satisfy. | ||
|
|
||
| **Owner: Phase 6** (the primitive, alongside §4/§5 — all three are the same | ||
| family of low-ceremony, pointer-shaped mechanism), **consumed pass-by-pass in | ||
| Phase 7** wherever profiling shows a real latency-hiding opportunity. | ||
|
|
||
| --- | ||
|
|
||
| ## Amendments from Phase 2 (2026-07-30) | ||
|
|
||
| Phase 1 said explicitly that "nothing here is load-bearing until Phase 2 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: drsnuggles8/OloEngineBase
Length of output: 14796
🌐 Web query:
Vulkan VK_KHR_buffer_device_address physicalStorageBuffer buffer device addresses spec💡 Result:
The Vulkan VK_KHR_buffer_device_address extension, which was promoted to core in Vulkan 1.2 [1][2], provides a mechanism for accessing buffer memory in shaders using 64-bit physical addresses, bypassing the traditional descriptor-based access model [3][4]. This system relies on three main components: 1. API Support (VK_KHR_buffer_device_address): Applications create a VkBuffer with the VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT flag and allocate memory with the VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT flag [1]. Using the command vkGetBufferDeviceAddress, the application retrieves a 64-bit VkDeviceAddress, which represents the start of the buffer [3][5]. This address can be passed to shaders (e.g., via constant buffers or push constants) [3][4]. 2. SPIR-V/Shading Language Support (SPV_KHR_physical_storage_buffer): To use these addresses in shaders, the SPIR-V code must utilize the PhysicalStorageBuffer storage class [3][1]. This is enabled by the PhysicalStorageBufferAddresses capability, which introduces the PhysicalStorageBuffer64 addressing model [6][7]. This model allows for hybrid addressing where pointers in this specific storage class are treated as 64-bit physical pointers, while other storage classes remain logical [6][7]. 3. Usage: Once the shader receives the VkDeviceAddress, it can perform direct reads, writes, and atomic operations on the memory at that location, provided the address is within the range of a valid buffer created with the appropriate usage flags [5][4]. This capability is essential for advanced features like ray tracing and implementing complex, pointer-based data structures directly in GPU memory [3]. Top results: [3][1][6][5][7][2][4]
Citations:
🌐 Web query:
Vulkan VK_KHR_shader_untyped_pointers spec physicalStorageBuffer storageBuffer💡 Result:
VK_KHR_shader_untyped_pointers does not introduce a special Vulkan descriptor type for physicalStorageBuffer vs storageBuffer; instead, it enables SPIR-V untyped pointers (OpTypeUntypedPointerKHR / OpUntypedVariableKHR / OpUntypedAccessChainKHR) so shaders can reinterpret memory through an “untyped” pointer while still starting from specific, layout-compatible storage classes/objects [1][2]. Key constraint tying it to physicalStorageBuffer storage buffers: 1) Unturned/untyped pointers are limited to storage classes that have an explicit layout. The VK_KHR_shader_untyped_pointers proposal states that it “limits the use of untyped pointers to storage classes with an explicit layout” to ensure consistent data layout across reinterpretations [1][3]. 2) SPIR-V’s untyped-pointer extension (SPV_KHR_untyped_pointers) defines that untyped pointers can be used with physical-storage-buffer-related pointer variables, but it also states restrictions on what variables/pointers may be declared with the PhysicalStorageBuffer storage class: - “OpVariable must not use the PhysicalStorageBuffer storage class. Variables must not use the PhysicalStorageBuffer storage class.” [2] This means you don’t declare a variable directly in the PhysicalStorageBuffer storage class when using SPV_KHR_untyped_pointers; rather, you use untyped-pointer constructs that can point to objects referenced via the PhysicalStorageBuffer addressing model. 3) For PhysicalStorageBuffer pointers (logical vs physical addressing), Vulkan’s physical storage buffer extension (SPV_KHR_physical_storage_buffer) adds the PhysicalStorageBuffer storage class and defines that it represents physical pointers (64-bit addresses) and only applies when the addressing model is PhysicalStorageBuffer64 [4]. So, how physicalStorageBuffer interacts with “storageBuffer” when using VK_KHR_shader_untyped_pointers: - Your storage buffer descriptor in Vulkan is still a storage buffer descriptor type (VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) described as a struct whose members are load/store/atomic in shaders [5]. - When you want physical addressing (PhysicalStorageBuffer / buffer device address) you use SPV_KHR_physical_storage_buffer (and Vulkan equivalents) to access buffer memory using PhysicalStorageBuffer pointers and buffer device addresses, with all loads/stores/atomics through PhysicalStorageBuffer pointers constrained to addresses that fall within the address range of some buffer [5]. - VK_KHR_shader_untyped_pointers then controls how you can reinterpret what you load/store through those pointer accesses, but only where the pointed-to storage class has explicit layout, and with SPIR-V-level constraints preventing direct PhysicalStorageBuffer variable declarations (OpVariable/Variables must not use the PhysicalStorageBuffer storage class) [1][2]. Practical implication (mapping to your terms): - “storageBuffer” (descriptor type / storage buffer block) is the logical storage buffer object the shader normally declares and indexes. - “physicalStorageBuffer” (PhysicalStorageBuffer storage class in SPIR-V) is about how buffer device addresses are treated for pointer addressing; with untyped pointers enabled, you can reinterpret the data via untyped pointer access chains, but you must follow SPIR-V restrictions that disallow declaring variables directly in the PhysicalStorageBuffer storage class [2] and you must ensure the relevant storage class used with untyped pointers has an explicit layout [1].
Citations:
🌐 Web query:
VK_KHR_untyped_buffer_host pointer VK_KHR_buffer_device_address difference💡 Result:
There is no technical feature or Vulkan extension named VK_KHR_untyped_buffer_host_pointer. It appears this may be a conflation of several different, distinct Vulkan extensions related to memory, pointers, and shaders. To clarify the concepts you may be combining: VK_KHR_buffer_device_address is a significant, widely used Vulkan feature (promoted to core in Vulkan 1.2) that allows applications to query a 64-bit GPU virtual address (VkDeviceAddress) for a VkBuffer [1][2][3]. This address can be used within shaders to access buffer memory via pointers, effectively bypassing standard descriptor sets and enabling complex data structures like linked lists or trees directly on the GPU [2][4]. VK_KHR_shader_untyped_pointers is a separate extension that introduces support for untyped pointers in SPIR-V [5][6]. It allows shader authors to reinterpret data accessed through memory and atomic instructions without requiring the strict type declarations typically associated with strongly-typed pointers [5][6]. This simplifies shader code by separating type interpretation from addressing logic [6]. VK_EXT_external_memory_host relates to importing memory allocated by the host (CPU) into Vulkan using a host pointer [7]. This is an entirely different mechanism used for zero-copy memory sharing between the CPU and GPU, rather than for shader-based pointer arithmetic or buffer addressing [7]. In summary, VK_KHR_buffer_device_address is about creating and using GPU-side pointers for buffers [2][4], while VK_KHR_shader_untyped_pointers is about the flexibility of pointer typing within shader code [5][6]. Neither extension provides the functionality of the other, and they are distinct from host memory import mechanisms [7].
Citations:
🌐 Web query:
Vulkan PhysicalStorageBuffer storage class spec PhysicalStorageBufferAddresses OpTypePointer PhysicalStorageBuffer shader code examples💡 Result:
The PhysicalStorageBuffer storage class is a feature of SPIR-V, introduced by the SPV_KHR_physical_storage_buffer extension, that enables "buffer device addressing" in Vulkan [1][2]. It allows shaders to access buffer memory using raw 64-bit GPU virtual addresses, bypassing standard descriptor sets [2][3][4]. PhysicalStorageBuffer Overview The storage class is used within the PhysicalStorageBuffer64 addressing model [1][5][6]. Unlike logical addressing (used in standard Vulkan), where pointers are abstract, PhysicalStorageBuffer64 treats pointers to the PhysicalStorageBuffer storage class as physical 64-bit addresses [1][2][6]. Key Technical Requirements: - Capability: Shaders using this must declare OpCapability PhysicalStorageBufferAddresses [2][5][6]. - Memory Model: The SPIR-V module must use the PhysicalStorageBuffer64 addressing model [1][6]. - Alignment: Because the compiler cannot infer the alignment of arbitrary physical addresses, all OpLoad and OpStore operations on PhysicalStorageBuffer pointers must explicitly specify an alignment (e.g., Aligned 8 or Aligned 16) [6]. - Restrictions: OpVariable cannot use the PhysicalStorageBuffer storage class [1][5]. Instead, these pointers are typically passed to the shader via push constants or stored in other buffers [2][3][4]. When used in a variable or function parameter, the pointer must be decorated with either AliasedPointer or RestrictPointer [1][5]. SPIR-V Code Example (Conceptual) The following illustrates defining a pointer type and accessing it, based on SPIR-V specification patterns [2][6]:; Declaration requirements OpCapability PhysicalStorageBufferAddresses OpExtension "SPV_KHR_physical_storage_buffer" OpMemoryModel PhysicalStorageBuffer64 GLSL450; Define the type to point to %int_type = OpTypeInt 32 0 %ptr_type = OpTypePointer PhysicalStorageBuffer %int_type; Example access (requires a base address provided to the shader); %ptr is a loaded 64-bit address, %val is the data to store OpStore %ptr %val Aligned 4 High-Level Usage (GLSL) In GLSL, this feature is accessed via the GL_EXT_buffer_reference extension [7][4]. Instead of standard descriptors, you define a structure with the buffer_reference layout qualifier [4]:
#extensionGL_EXT_buffer_reference: enable layout(buffer_reference, std430) buffer MyData { vec4 value; }; layout(push_constant) uniform Constants { MyData myPtr; // Receives 64-bit address }; void main { // Access directly via the physical pointer vec4 v = myPtr.value; } This mechanism is essential for complex data structures like linked lists, trees, or graphs that are difficult to implement with static descriptor sets [7][4]. [1][2][4][6]Citations:
🌐 Web query:
SPV_KHR_physical_storage_buffer PhysicalStorageBuffer variable declaration StorageBuffer storage class💡 Result:
The SPV_KHR_physical_storage_buffer extension introduces the PhysicalStorageBuffer storage class, which functions differently from the standard StorageBuffer storage class used in Vulkan and other SPIR-V environments [1][2]. Storage Class Distinction The PhysicalStorageBuffer storage class is specifically designed to support physical addressing (using 64-bit addresses), whereas the StorageBuffer class relies on logical addressing [1][2]. While StorageBuffer is typically used for standard shader storage buffer objects (SSBOs) accessed through descriptors, PhysicalStorageBuffer is used for buffer device address (BDA) pointers, allowing shaders to hold and dereference raw device memory addresses [2][3]. Variable Declaration Rules The extension explicitly prohibits the use of PhysicalStorageBuffer with OpVariable in the same way as standard storage classes [1][4]. Specifically: 1. OpVariable Declaration: An OpVariable must not use the PhysicalStorageBuffer storage class directly [1][4]. 2. Memory Object Definition: A memory object in this context is defined as an OpVariable (or function parameter) that holds a pointer to the PhysicalStorageBuffer storage class, or an array of such pointers [1][4]. 3. Aliasing Decorators: When an OpVariable is used to store a pointer of the PhysicalStorageBuffer storage class, the variable must be decorated with exactly one of either AliasedPointer or RestrictPointer [1][5]. This is necessary because pointers in this storage class are often loaded from memory or generated via conversion (e.g., OpConvertUToPtr) rather than being created directly by an OpVariable, requiring these decorations to clarify potential memory aliasing [1][4]. In contrast, a standard StorageBuffer variable is declared normally using OpVariable with a pointer type in the StorageBuffer storage class [6][7].
Citations:
Declare the buffer device-address capability contract.
VK_KHR_shader_untyped_pointersis not the API path for buffer device addresses. The root struct can still pass an address through push constants, but addressable buffers needVK_KHR_buffer_device_addresson the Vulkan side, plusPhysicalStorageBufferAddresses,PhysicalStorageBuffer64, buffer usage flags, allocation flags, and alignment/lifetime rules. Add those to this ADR or ADR 0010 so the contract matches the implementation.🤖 Prompt for AI Agents