fix(render3d): nothing draws into a BC7 texture - sampler binding binds, and compressed images get no mips

Metal aborted a net.sh guest ("MTLPixelFormatBC7_RGBAUnorm is not color renderable"): the crash
report's main thread was in vkQueueSubmit from gvk_once_end, MoltenVK encoding a vkCmdBlitImage
through a render pipeline. water.ludic bound its reflection by sampler name and then asked the
BOUND texture for mips - on Vulkan the bound texture was not the reflection but the last one
bound, since 23.3 a BC7 kit texture. gpu_bind_sampler now makes its texture the bound one, as
OpenGL did (and water binds it explicitly); gvk_tex_mips and gvk_mips_now skip compressed images;
gvk_pass_begin leaves out a compressed colour attachment and says so.

tests/net.sh passes (main e515bd87 built against it; host and guest agree on 196 animals, the
people, the board and the lost hiker).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 10:34:23 +03:00
parent f1049953c4
commit 1447304dd7
6 changed files with 40 additions and 0 deletions

View file

@ -1094,10 +1094,29 @@ function gvk_pass_collect(render3d_st: mut Render3dState, fb: int, rec: words, r
render3d_st.gvk_pass_dep[1] = rec[rec_o + 3]
}
# A block-compressed image cannot be drawn into (Metal aborts, "BC7 is not color renderable"):
# an attachment that is one is dropped from the pass, and said, with what it was
function gvk_pass_no_compressed(render3d_st: mut Render3dState) -> void {
var w = 0
for c in 0 .. render3d_st.gvk_pass_ncolor {
let tex = render3d_st.gvk_pass_col[c * 2]
if tex > 0 and tex < len(render3d_st.gvk_tex_glfmt) and gvk_is_compressed(render3d_st.gvk_tex_glfmt[tex]) {
if render3d_st.gvk_bc_said < 8 {
render3d_st.gvk_bc_said += 1
print(`r3d: vulkan: framebuffer {render3d_st.gvk_fb_cur} would draw into compressed texture {tex} ({gvk_tex_w(render3d_st, tex)}x{gvk_tex_h(render3d_st, tex)}); left out`)
}
} else {
render3d_st.gvk_pass_col[w * 2] = tex; render3d_st.gvk_pass_col[w * 2 + 1] = render3d_st.gvk_pass_col[c * 2 + 1]
w += 1
}
}
render3d_st.gvk_pass_ncolor = w
}
function gvk_pass_begin(render3d_st: mut Render3dState, rec: words, rec_o: int) -> void {
if render3d_st.gvk_in_pass { return }
let cb = gvk_frame_cb(render3d_st)
gvk_pass_collect(render3d_st, render3d_st.gvk_fb_cur, rec, rec_o)
gvk_pass_no_compressed(render3d_st)
let aw = VkRenderingAttachmentInfo_sizeof
let catt = gvk_tmp(render3d_st, aw * 3)
Vk.zero(catt, aw * 3)
@ -1818,6 +1837,7 @@ function gvk_resize_check(render3d_st: mut Render3dState) -> bool {
# smallest level every frame): recorded into the open frame after its pass, so they cost no submit.
# A chain that has to grow first still goes through its one-shot path.
function gvk_mips_now(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> void {
if gvk_is_compressed(render3d_st.gvk_tex_glfmt[tex]) { return } # its levels came with it
if render3d_st.gvk_cb == null or render3d_st.gvk_tex_levels[tex] <= 1 { gvk_flush(render3d_st); gvk_tex_mips(render3d_st, tex, w, h); return }
gvk_pass_end(render3d_st)
gvk_tex_mips_into(render3d_st, render3d_st.gvk_cb, tex, w, h)