feat(render3d): R3D_GFX=vk draws the frame through Vulkan, headless

gpu.ludic's calls branch to the Vulkan backend when it was chosen and came up; every OpenGL
statement is unchanged, only guarded. R3D_GFX=vk selects it in a headless run (the window's
swapchain is the next milestone) and falls back to OpenGL, with the reason, when the device or
the SPIR-V manifest is missing.

- Render state is cached as before and turned into pipelines at the draw; u_* and sampler binds
  go into the variant's uniform blocks; meshes, buffers and textures are gvk_* objects;
  framebuffer binds are dynamic-rendering passes; same-size blits are image copies; the screen,
  the photograph read-back, the present and the screenshot go through the frame.
- Work that submits on its own (uploads, read-backs, new or freed images and buffers) flushes the
  frame first, so it runs in OpenGL's order. A read may take fewer channels than the image has
  (the height field's R from its RGBA32F bake). Pipeline keys name vertex bindings by order, not
  buffer handle, so re-pointed instance buffers keep their pipeline.

The valley renders at frame 90 validation-clean on the RTX 3070 Ti and on MoltenVK. OpenGL frames
byte-identical at the five viewpoints; 59 self-tests pass with no GL error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 12:20:16 +03:00
parent 451bc9063c
commit 072f9a848e
3 changed files with 290 additions and 73 deletions

View file

@ -328,7 +328,10 @@ function gvk_tex_mips(tex: int, w: int, h: int) -> bool {
function gvk_tex_read(tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, out: pointer) -> bool {
let cout = gvk_channels(ifmt)
let bout = gvk_channel_bytes(ifmt)
if gvk_gl_channels(fmt) != cout or gvk_gl_type_bytes(ty) != bout {
let cwant = gvk_gl_channels(fmt)
# a read may ask for fewer channels than the image has (the height field's R of an RGBA32F
# bake), never more, and only in the sample size the image stores
if cwant > cout or gvk_gl_type_bytes(ty) != bout {
print(`r3d: vulkan: no read-back conversion for GL format {ifmt} as {fmt}/{ty}`)
return false
}
@ -350,7 +353,12 @@ function gvk_tex_read(tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, ou
Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, gvk_st_buf, 1, bic)
gvk_barrier(cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
let ok = gvk_once_end(cb)
if ok { mem_copy(out, src, n) }
if ok and cwant == cout { mem_copy(out, src, n) }
if ok and cwant < cout {
let texel = cout * bout
let keep = cwant * bout
for t in 0 .. w * h { mem_copy(mem_off(out, t * keep), mem_off(src, t * texel), keep) }
}
gvk_staging_free()
return ok
}