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:
parent
072f9a848e
commit
a674c9b372
33 changed files with 70 additions and 7 deletions
|
|
@ -61,6 +61,19 @@ program VkResources {
|
|||
check("RGB16F from floats (halved)", gvk_tex_storage(t4, false, GL_RGB16F, w, h, 1, true) and gvk_tex_upload(t4, GL_RGB16F, w, h, 1, GL_RGB, GL_FLOAT, fl))
|
||||
check("half of 1.5 is 0x3E00", gvk_half(0x3FC00000) == 0x3E00)
|
||||
|
||||
# a 16-bit RGB PNG (a normal map): big-endian samples, swapped on upload, alpha filled opaque
|
||||
let n16 = bytes(w * h * 6)
|
||||
for i in 0 .. w * h * 3 { n16[i * 2] = (i >> 8) & 255; n16[i * 2 + 1] = i & 255 }
|
||||
let t7 = gvk_tex_new()
|
||||
gvk_unpack_swap = true
|
||||
check("RGB16 swapped upload", gvk_tex_storage(t7, false, GL_RGB16, w, h, 1, true) and gvk_tex_upload(t7, GL_RGB16, w, h, 1, GL_RGB, GL_UNSIGNED_SHORT, n16))
|
||||
gvk_unpack_swap = false
|
||||
let b16 = bytes(w * h * 6)
|
||||
check("RGB16 read-back call", gvk_tex_read(t7, GL_RGB16, w, h, GL_RGB, GL_UNSIGNED_SHORT, b16))
|
||||
var s16 = true
|
||||
for i in 0 .. w * h * 3 { if b16[i * 2] != (i & 255) or b16[i * 2 + 1] != ((i >> 8) & 255) { s16 = false } }
|
||||
check("RGB16 samples arrive little-endian", s16)
|
||||
|
||||
# the shadow cascades: a depth array, and a render-sized half-float target with no pixels
|
||||
let t5 = gvk_tex_new()
|
||||
check("D32 array of 4", gvk_tex_storage(t5, true, GL_DEPTH_COMPONENT32F, 256, 256, 4, false))
|
||||
|
|
@ -85,7 +98,7 @@ program VkResources {
|
|||
check("a smaller upload keeps the buffer", gvk_buf_upload(b, 512, data) and gvk_buf[b] == first)
|
||||
check("a larger upload grows it", gvk_buf_upload(b, 4096, data) and gvk_buf_size[b] >= 4096)
|
||||
|
||||
for t in 1 .. 7 { gvk_tex_release(t) }
|
||||
for t in 1 .. 8 { gvk_tex_release(t) }
|
||||
gvk_buf_release(b)
|
||||
for i in 0 .. len(gvk_smp) { Vk.destroy_sampler(gvk_dev, gvk_smp[i], null) }
|
||||
print(`vulkan: {gvk_n_allocs} allocations left after freeing`)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -----------------------------------------------------------------------------
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -173,7 +173,9 @@ function cmd_shaders() -> int {
|
|||
# is untouched. No y flip: a Vulkan target's row 0 is where OpenGL's is (NDC y = -1), so render
|
||||
# to texture, sampling and gl_FragCoord agree; only the present and the screenshot flip.
|
||||
let depth_remap = "\n#undef main\nvoid main() {\n ludic_gl_main();\n gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;\n}\n"
|
||||
let vsrc = "#version 460\n" + defines + "#define main ludic_gl_main\n" + shd_file(parts[0]) + depth_remap
|
||||
# invariant: the foliage's depth prepass and its lit pass (depth EQUAL) are different variants,
|
||||
# and without it the compiler may compute their positions a hair apart and fail the test
|
||||
let vsrc = "#version 460\n" + defines + "invariant gl_Position;\n#define main ludic_gl_main\n" + shd_file(parts[0]) + depth_remap
|
||||
let fsrc = "#version 460\n" + defines + shd_noise + shd_lighting + shd_file(parts[1])
|
||||
# one binding table for both stages
|
||||
shd_names = new []pointer
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue