fix(render3d): Vulkan exposure, foliage depth and meadow alpha

Three things the first Vulkan frames on the RTX 3070 Ti showed against OpenGL on the same PC:

- Exposure: the adaptation pass reads the HDR scene's smallest mip, and a render target made
  without pixels had one level, so exposure came from a single texel. A target asked for mipmaps
  now grows a full chain (level 0 kept), and passes draw through level-0 views keyed by the
  image's generation.
- Foliage: the depth prepass and the lit pass (depth EQUAL) are different variants. Vulkan vertex
  stages now declare an invariant gl_Position so both land on the same depth.
- Alpha to coverage is enabled only on a multisampled pass. OpenGL ignores it without MSAA; Vulkan
  with one sample dropped every fragment under half alpha.

vk_resources also checks a big-endian 16-bit RGB upload (a normal map). VKRES OK; ludic-dev test
140 passed; the PC's Vulkan frame is validation-clean; OpenGL frames byte-identical at the five
viewpoints with 59 self-tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 12:29:24 +03:00
parent 072f9a848e
commit a674c9b372
33 changed files with 70 additions and 7 deletions

View file

@ -290,7 +290,9 @@ function gvk_pipeline(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: in
Vk.zero(ms, VkPipelineMultisampleStateCreateInfo_sizeof)
Vk.put_i32(ms, VkPipelineMultisampleStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO)
Vk.put_i32(ms, VkPipelineMultisampleStateCreateInfo_rasterizationSamples, samples)
Vk.put_i32(ms, VkPipelineMultisampleStateCreateInfo_alphaToCoverageEnable, st.a2c)
# OpenGL ignores alpha to coverage without a multisampled target; Vulkan with one sample would
# instead drop every fragment under half alpha, which erased the meadow's flowers
if samples > 1 { Vk.put_i32(ms, VkPipelineMultisampleStateCreateInfo_alphaToCoverageEnable, st.a2c) }
let ds = bytes(VkPipelineDepthStencilStateCreateInfo_sizeof)
Vk.zero(ds, VkPipelineDepthStencilStateCreateInfo_sizeof)
Vk.put_i32(ds, VkPipelineDepthStencilStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO)
@ -607,10 +609,12 @@ function gvk_frame_cb() -> pointer {
return gvk_cb
}
# a view of one layer of an array image, for a cascade drawn into on its own
# The view a pass draws into: level 0 only (an attachment view has exactly one level, and a target
# the exposure measure mipmaps has several), and one layer of an array image for a cascade drawn
# on its own. Keyed by the image's generation, so a replaced image never reuses a stale view.
function gvk_view_of(tex: int, layer1: int) -> long {
if layer1 == 0 { return gvk_tex_view[tex] }
let key = `{tex}:{layer1}`
if layer1 == 0 and gvk_tex_levels[tex] <= 1 and gvk_tex_layers[tex] <= 1 { return gvk_tex_view[tex] }
let key = `{tex}:{gvk_tex_gen[tex]}:{layer1}`
if gvk_layer_views == null { gvk_layer_views = new []string; gvk_layer_view = new []long }
for i in 0 .. len(gvk_layer_views) { if gvk_layer_views[i] == key { return gvk_layer_view[i] } }
let depth = gvk_tex_vkfmt[tex] == VK_FORMAT_D32_SFLOAT
@ -623,7 +627,7 @@ function gvk_view_of(tex: int, layer1: int) -> long {
let sr = VkImageViewCreateInfo_subresourceRange
if depth { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
Vk.put_i32(vci, sr + VkImageSubresourceRange_levelCount, 1)
Vk.put_i32(vci, sr + VkImageSubresourceRange_baseArrayLayer, layer1 - 1)
if layer1 > 0 { Vk.put_i32(vci, sr + VkImageSubresourceRange_baseArrayLayer, layer1 - 1) }
Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, 1)
let out = bytes(8)
let zero: long = 0

View file

@ -52,6 +52,9 @@ var gvk_tex_layers: []int = null
var gvk_tex_vkfmt: []int = null
var gvk_tex_dims_w: []int = null
var gvk_tex_dims_h: []int = null
var gvk_tex_glfmt: []int = null # the format the renderer asked for (gpu.ludic's vocabulary)
var gvk_tex_array: []int = null # 1 for an array image
var gvk_tex_gen: []int = null # bumped whenever the image behind a handle is replaced
var gvk_unpack_swap: bool = false # GL_UNPACK_SWAP_BYTES: 16-bit PNG samples arrive big-endian
function gvk_tex_new() -> int {
@ -60,7 +63,9 @@ function gvk_tex_new() -> int {
gvk_tex_image = new []long; gvk_tex_view = new []long; gvk_tex_mem = new []long
gvk_tex_levels = new []int; gvk_tex_layers = new []int; gvk_tex_vkfmt = new []int
gvk_tex_dims_w = new []int; gvk_tex_dims_h = new []int
gvk_tex_glfmt = new []int; gvk_tex_array = new []int; gvk_tex_gen = new []int
push(gvk_tex_dims_w, 0); push(gvk_tex_dims_h, 0)
push(gvk_tex_glfmt, 0); push(gvk_tex_array, 0); push(gvk_tex_gen, 0)
# handle 0 is "no texture", as it is on OpenGL
push(gvk_tex_image, zero); push(gvk_tex_view, zero); push(gvk_tex_mem, zero)
push(gvk_tex_levels, 0); push(gvk_tex_layers, 0); push(gvk_tex_vkfmt, 0)
@ -68,6 +73,7 @@ function gvk_tex_new() -> int {
push(gvk_tex_image, zero); push(gvk_tex_view, zero); push(gvk_tex_mem, zero)
push(gvk_tex_levels, 0); push(gvk_tex_layers, 0); push(gvk_tex_vkfmt, 0)
push(gvk_tex_dims_w, 0); push(gvk_tex_dims_h, 0)
push(gvk_tex_glfmt, 0); push(gvk_tex_array, 0); push(gvk_tex_gen, 0)
return len(gvk_tex_image) - 1
}
@ -191,6 +197,8 @@ function gvk_tex_storage(tex: int, array: bool, ifmt: int, w: int, h: int, layer
gvk_tex_image[tex] = image; gvk_tex_view[tex] = gvk_handle(out); gvk_tex_mem[tex] = mem
gvk_tex_levels[tex] = levels; gvk_tex_layers[tex] = layers; gvk_tex_vkfmt[tex] = vkfmt
gvk_tex_dims_w[tex] = w; gvk_tex_dims_h[tex] = h
gvk_tex_glfmt[tex] = ifmt; gvk_tex_gen[tex] = gvk_tex_gen[tex] + 1
if array { gvk_tex_array[tex] = 1 } else { gvk_tex_array[tex] = 0 }
# a target starts cleared-to-nothing but readable: every level in the layout samplers expect
let cb = gvk_once_begin()
gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
@ -285,7 +293,42 @@ function gvk_tex_upload(tex: int, ifmt: int, w: int, h: int, layers: int, fmt: i
}
# The mip chain from level 0, each level blitted down from the one above it
# A target made without pixels has one level; the renderer asking it for mipmaps (the exposure
# measure reads the HDR scene's smallest level every frame) grows it a full chain, level 0 kept.
function gvk_tex_grow_mips(tex: int, w: int, h: int) -> bool {
let old_image = gvk_tex_image[tex]
let old_view = gvk_tex_view[tex]
let old_mem = gvk_tex_mem[tex]
let layers = gvk_tex_layers[tex]
let zero: long = 0
gvk_tex_image[tex] = zero
if not gvk_tex_storage(tex, gvk_tex_array[tex] == 1, gvk_tex_glfmt[tex], w, h, layers, true) { return false }
let cb = gvk_once_begin()
gvk_barrier(cb, old_image, false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
gvk_barrier(cb, gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let ic = bytes(VkImageCopy_sizeof)
Vk.zero(ic, VkImageCopy_sizeof)
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_layerCount, layers)
Vk.put_i32(ic, VkImageCopy_dstSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(ic, VkImageCopy_dstSubresource + VkImageSubresourceLayers_layerCount, layers)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_width, w)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1)
Vk.cmd_copy_image(cb, old_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, gvk_tex_image[tex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
gvk_barrier(cb, gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
let ok = gvk_once_end(cb)
Vk.destroy_image_view(gvk_dev, old_view, null)
Vk.destroy_image(gvk_dev, old_image, null)
Vk.free_memory(gvk_dev, old_mem, null)
gvk_n_allocs -= 1
return ok
}
function gvk_tex_mips(tex: int, w: int, h: int) -> bool {
if gvk_tex_levels[tex] <= 1 and (w > 1 or h > 1) and not gvk_is_depth(gvk_tex_glfmt[tex]) {
if not gvk_tex_grow_mips(tex, w, h) { return false }
}
let levels = gvk_tex_levels[tex]
if levels <= 1 { return true }
let image = gvk_tex_image[tex]
@ -372,6 +415,7 @@ function gvk_tex_release(tex: int) -> void {
gvk_n_allocs -= 1
gvk_tex_image[tex] = zero; gvk_tex_view[tex] = zero; gvk_tex_mem[tex] = zero
gvk_tex_levels[tex] = 0; gvk_tex_layers[tex] = 0; gvk_tex_vkfmt[tex] = 0
gvk_tex_gen[tex] = gvk_tex_gen[tex] + 1
}
# ---- samplers -----------------------------------------------------------------------------