Parent
Implement the scene plugin
What happens
A render pass with a depth attachment and no colour attachment kills the
process. Not a validation error, a fault: an access violation inside the driver,
several frames of stack away from anything that names a pass.
github.com/gogpu/wgpu/hal/vulkan/vk.(*Commands).CmdEndRenderPass
github.com/gogpu/wgpu/hal/vulkan.(*RenderPassEncoder).End
github.com/dvoyni/cog/wgpu.(*gfxBackend).EndPass wgpu/gfxpass.go
github.com/dvoyni/cog/gfx.(*GpuQueue).ReplayPasses gfx/gpuqueue.go
Why
hal/vulkan.(*CommandEncoder).BeginRenderPass returns a render-pass encoder
without beginning a render pass when the descriptor names no colour
attachments:
if e.active == 0 || len(desc.ColorAttachments) == 0 {
return rpe
}
RenderPassEncoder.End then calls vkCmdEndRenderPass unconditionally, on a
pass that was never begun. Verified present in the pinned
v0.31.6-0.20260827093430 and still present in v0.34.4, so it is a live
upstream gap rather than a stale pin. A browser's own WebGPU encodes the same
descriptor correctly, so the wasm build of the same demo runs the pass.
cog/wgpu.BeginPass already emits the descriptor correctly — passColour
returns nil and the pass is encoded with an empty ColorAttachments and a depth
attachment only. Everything above the HAL is right.
Two shapes reach it, and only one of them is deliberate
The second is the more serious, because nothing in the tree asked for it:
GpuPassDesc could not tell NoTarget() from a texture target whose view
does not exist yet — both resolve to a zero TextureViewID. And every
TemporaryTarget is unresolved on its first frame, because its allocation is a
bake the backend replays after the pass descriptors were built. So the first
frame of any app that renders into a temporary target with depth already emitted
a depth-only descriptor, and already faulted. It has not been seen before only
because nothing in the tree used a temporary target as a camera's colour target
until now.
What was done
Containment, in cog, because the fix is upstream:
GpuPassDesc.NoColor says a pass declares no colour attachment at all, set
from head.Target.kind == targetNone, so a zero Target is no longer
readable as "depth-only".
wgpu.BeginPass returns nil when the colour attachment does not resolve, for
either reason. gfx already skips a pass whose BeginPass returned nil.
- A pass that asked for it deliberately (
NoColor) reports
ErrDepthOnlyPassUnsupported once per run, through the plugin, which has a
kernel handle where the backend does not. The unresolved-view case is silent:
it is a frame-one artefact of allocation order and there is nothing for a
caller to do about it.
- The refusal is gated on
depthOnlyPassSupported, a per-platform constant:
false on !js, true on js. The browser runs the pass.
What is still not fixed
A depth-only pass does not execute on the desktop. Its depth texture is left
untouched, so a later pass that loads that depth with LoadPreserve renders
against undefined depth — the whole pass, not one draw. Until the HAL gap
closes, a depth-only pass may be written but its output may not be depended on
in the same frame. cog/scene/spec.md § Passes carries the rule.
This is the constraint shadow maps will have to negotiate, so this ticket stays
the place that records it.
Acceptance criteria
Parent
Implement the scene plugin
What happens
A render pass with a depth attachment and no colour attachment kills the
process. Not a validation error, a fault: an access violation inside the driver,
several frames of stack away from anything that names a pass.
Why
hal/vulkan.(*CommandEncoder).BeginRenderPassreturns a render-pass encoderwithout beginning a render pass when the descriptor names no colour
attachments:
RenderPassEncoder.Endthen callsvkCmdEndRenderPassunconditionally, on apass that was never begun. Verified present in the pinned
v0.31.6-0.20260827093430and still present inv0.34.4, so it is a liveupstream gap rather than a stale pin. A browser's own WebGPU encodes the same
descriptor correctly, so the wasm build of the same demo runs the pass.
cog/wgpu.BeginPassalready emits the descriptor correctly —passColourreturns nil and the pass is encoded with an empty
ColorAttachmentsand a depthattachment only. Everything above the HAL is right.
Two shapes reach it, and only one of them is deliberate
The second is the more serious, because nothing in the tree asked for it:
GpuPassDesccould not tellNoTarget()from a texture target whose viewdoes not exist yet — both resolve to a zero
TextureViewID. And everyTemporaryTargetis unresolved on its first frame, because its allocation is abake the backend replays after the pass descriptors were built. So the first
frame of any app that renders into a temporary target with depth already emitted
a depth-only descriptor, and already faulted. It has not been seen before only
because nothing in the tree used a temporary target as a camera's colour target
until now.
What was done
Containment, in
cog, because the fix is upstream:GpuPassDesc.NoColorsays a pass declares no colour attachment at all, setfrom
head.Target.kind == targetNone, so a zeroTargetis no longerreadable as "depth-only".
wgpu.BeginPassreturns nil when the colour attachment does not resolve, foreither reason. gfx already skips a pass whose
BeginPassreturned nil.NoColor) reportsErrDepthOnlyPassUnsupportedonce per run, through the plugin, which has akernel handle where the backend does not. The unresolved-view case is silent:
it is a frame-one artefact of allocation order and there is nothing for a
caller to do about it.
depthOnlyPassSupported, a per-platform constant:false on
!js, true onjs. The browser runs the pass.What is still not fixed
A depth-only pass does not execute on the desktop. Its depth texture is left
untouched, so a later pass that loads that depth with
LoadPreserverendersagainst undefined depth — the whole pass, not one draw. Until the HAL gap
closes, a depth-only pass may be written but its output may not be depended on
in the same frame.
cog/scene/spec.md§ Passes carries the rule.This is the constraint shadow maps will have to negotiate, so this ticket stays
the place that records it.
Acceptance criteria
GpuPassDescdistinguishes "no colour attachment" from "colour attachmentnot resolved yet"
BeginRenderPassas a depth-only descriptor on thenative path
hal/vulkanbegins a render pass with no colour attachments