diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 96c07802..1f8a9559 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -191,7 +191,13 @@ export state Render3dState { gvk_frame_fence: bytes = null # the presented frame still on the GPU (one frame in flight) gvk_frame_pending: bool = false gvk_frame_pending_cb: pointer = null - gvk_inflight: int = -1 # -1 until asked: 1 leaves the presented frame in flight + gvk_frame_pending_no: int = -1 # which frame it is (gvk_frame_no when it was submitted) + gvk_dpools: []long = null # a descriptor pool per frame slot (frame_no & 1) + gvk_ring_end: int = 0 # the end of this frame's half of the ring + gvk_retired_frame: []int = null # the last frame that read each retired buffer + gvk_labels: bool = false # R3D_VK_LABELS: each profiled pass is a debug label (Metal System Trace) + gvk_inflight: int = -1 + gvk_label_depth: int = 0 # -1 until asked: 1 leaves the presented frame in flight gvk_has_hqr: bool = false gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick gvk_qpool: long = 0 @@ -582,6 +588,8 @@ export state Render3dState { sc_cast_dy: float = 0.0 sc_cast_k: float = 0.0 sc_skip_blade: bool = false + sc_skip_grass: bool = false + grass_force: bool = false # R3D_BLADES: the blades on whatever the game set sc_skip_card: bool = false cb_state: int = 12345 shadow_res: int = 2048 diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index 2793f781..87f2b6d5 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -85,6 +85,9 @@ function gvk_init(render3d_st: mut Render3dState) -> bool { Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_SURFACE_EXTENSION_NAME); n_iext += 1 Vk.put_ptr(iext_names, n_iext * 8, surf_ext); n_iext += 1 } + # R3D_VK_LABELS: name each pass for a GPU capture (MoltenVK makes them Metal debug groups) + render3d_st.gvk_labels = r3d_env_has(render3d_st, "R3D_VK_LABELS") and gvk_ext_in(iexts, nie, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) and Vk.has("vkCmdBeginDebugUtilsLabelEXT") == 1 + if render3d_st.gvk_labels { Vk.put_ptr(iext_names, n_iext * 8, VK_EXT_DEBUG_UTILS_EXTENSION_NAME); n_iext += 1 } # HDR output: an instance only lists the HDR colour spaces when it asks for them render3d_st.gvk_has_colorspace = render3d_st.gvk_has_surface and gvk_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) if render3d_st.gvk_has_colorspace { Vk.put_ptr(iext_names, n_iext * 8, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); n_iext += 1 } @@ -544,8 +547,11 @@ function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool { # CPU while the GPU draws: waiting at the present serialised the two, which cost MoltenVK a third # of its frame rate against OpenGL. Anything that would reuse what that frame reads - the next # frame's command buffer and ring, a one-off submit, a descriptor pool reset, a swapchain rebuild - -# waits for it first (gvk_frame_wait), and a buffer it reads is busy (gvk_buf_busy). On by default -# on macOS; R3D_VK_INFLIGHT=0/1 says otherwise. +# waits for it first (gvk_frame_wait), and a buffer it reads is busy (gvk_buf_busy). The frame's +# uniform ring and descriptor pool are double-buffered by frame_no & 1, so the next frame records +# while this one draws: waiting at the next frame's start left the GPU idle for the whole of its +# recording and MoltenVK's encoding (a third of a frame). On by default on macOS; R3D_VK_INFLIGHT=0/1 +# says otherwise. function gvk_inflight_on(render3d_st: mut Render3dState) -> bool { if render3d_st.gvk_inflight < 0 { render3d_st.gvk_inflight = 0 @@ -571,6 +577,7 @@ function gvk_frame_submit(render3d_st: mut Render3dState, cb: pointer) -> bool { if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkQueueSubmit", r) } render3d_st.gvk_frame_pending = true render3d_st.gvk_frame_pending_cb = cb + render3d_st.gvk_frame_pending_no = render3d_st.gvk_frame_no return true } @@ -583,7 +590,7 @@ function gvk_frame_wait(render3d_st: mut Render3dState) -> void { Vk.free_command_buffers(render3d_st.gvk_dev, render3d_st.gvk_pool, 1, cbs) render3d_st.gvk_frame_pending = false render3d_st.gvk_frame_pending_cb = null - gvk_retire_flush(render3d_st) + gvk_retire_upto(render3d_st, render3d_st.gvk_frame_pending_no) } # ---- teardown ----------------------------------------------------------------------------- @@ -640,3 +647,30 @@ function gvk_query_result(render3d_st: Render3dState, id: int, out: words) -> bo out[0] = int(float(ticks) * render3d_st.gvk_ts_period) return true } + +# ---- debug labels (R3D_VK_LABELS) ------------------------------------------------------------ +function gvk_label_begin(render3d_st: mut Render3dState, name: pointer) -> void { + # only inside a frame: a pass the load profiles comes before the frame's pools exist + if render3d_st.gpu_kind != GPU_VK or render3d_st.gvk_cb == null { return } + let cb = render3d_st.gvk_cb + let li = bytes(VkDebugUtilsLabelEXT_sizeof) + Vk.zero(li, VkDebugUtilsLabelEXT_sizeof) + Vk.put_i32(li, VkDebugUtilsLabelEXT_sType, VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT) + Vk.put_ptr(li, VkDebugUtilsLabelEXT_pLabelName, name) + Vk.cmd_begin_debug_utils_label_ext(cb, li) + render3d_st.gvk_label_depth += 1 +} +function gvk_label_end(render3d_st: mut Render3dState) -> void { + if render3d_st.gpu_kind != GPU_VK or render3d_st.gvk_label_depth <= 0 or render3d_st.gvk_cb == null { return } + Vk.cmd_end_debug_utils_label_ext(render3d_st.gvk_cb) + render3d_st.gvk_label_depth -= 1 +} +# the frame's command buffer ends with every label it opened closed (called where it ends: a +# pointer compares by what it points at, so once_end cannot ask which buffer it was handed) +function gvk_labels_close(render3d_st: mut Render3dState) -> void { + if render3d_st.gvk_cb == null { render3d_st.gvk_label_depth = 0; return } + while render3d_st.gvk_label_depth > 0 { + Vk.cmd_end_debug_utils_label_ext(render3d_st.gvk_cb) + render3d_st.gvk_label_depth -= 1 + } +} diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index 8efb6e85..967ee298 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -650,7 +650,8 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { if (counts & VK_SAMPLE_COUNT_2_BIT) != 0 { render3d_st.gvk_msaa_max = 2 } if (counts & VK_SAMPLE_COUNT_4_BIT) != 0 { render3d_st.gvk_msaa_max = 4 } render3d_st.gvk_ring_buf = gvk_buf_new(render3d_st) - if not gvk_buf_reserve(render3d_st, render3d_st.gvk_ring_buf, GVK_RING_BYTES) { return false } + # one half per frame slot: the cached sets bind this one buffer, and each draw's offset says the half + if not gvk_buf_reserve(render3d_st, render3d_st.gvk_ring_buf, GVK_RING_BYTES * 2) { return false } let sizes = bytes(VkDescriptorPoolSize_sizeof * 3) Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, 32768) @@ -665,9 +666,13 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 3) Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes) let out = bytes(8) - let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out) - if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) } - render3d_st.gvk_dpool = gvk_handle(out) + render3d_st.gvk_dpools = new []long + for k in 0 .. 2 { + let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out) + if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) } + push(render3d_st.gvk_dpools, gvk_handle(out)) + } + render3d_st.gvk_dpool = render3d_st.gvk_dpools[0] if not gvk_kpool_make(render3d_st) { return false } render3d_st.gvk_white = gvk_tex_new(render3d_st) let px = bytes(4) @@ -676,14 +681,17 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { } function gvk_frame_reset(render3d_st: mut Render3dState) -> void { - render3d_st.gvk_ring_off = 0 + let slot = render3d_st.gvk_frame_no & 1 + render3d_st.gvk_ring_off = slot * GVK_RING_BYTES + render3d_st.gvk_ring_end = (slot + 1) * GVK_RING_BYTES + render3d_st.gvk_dpool = render3d_st.gvk_dpools[slot] Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_dpool, 0) } # a block into the ring; its offset, or -1 when the frame has used the whole ring function gvk_ring_put(render3d_st: mut Render3dState, blk: pointer, n: int) -> int { let at = (render3d_st.gvk_ring_off + render3d_st.gvk_ring_align - 1) / render3d_st.gvk_ring_align * render3d_st.gvk_ring_align - if at + n > GVK_RING_BYTES { return -1 } + if at + n > render3d_st.gvk_ring_end { return -1 } mem_copy(mem_off(render3d_st.gvk_buf_map[render3d_st.gvk_ring_buf], at), blk, n) render3d_st.gvk_ring_off = at + n return at @@ -954,7 +962,8 @@ function gvk_screen_make(render3d_st: mut Render3dState, w: int, h: int) -> bool function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer { if render3d_st.gvk_cb == null { - gvk_frame_wait(render3d_st) + # the frame in flight uses the other half of the ring and the other pool, unless it has this slot + if render3d_st.gvk_frame_pending and (render3d_st.gvk_frame_pending_no & 1) == (render3d_st.gvk_frame_no & 1) { gvk_frame_wait(render3d_st) } gvk_frame_reset(render3d_st) render3d_st.gvk_cb = gvk_once_begin(render3d_st) } @@ -1270,6 +1279,7 @@ function gvk_flush(render3d_st: mut Render3dState) -> void { if render3d_st.gvk_cb == null { return } render3d_st.gvk_n_flush += 1 gvk_pass_end(render3d_st) + gvk_labels_close(render3d_st) gvk_once_end(render3d_st, render3d_st.gvk_cb) render3d_st.gvk_cb = null render3d_st.gvk_frame_no += 1 @@ -1290,6 +1300,7 @@ function gvk_present(render3d_st: mut Render3dState) -> void { } if render3d_st.gvk_cb == null { return } gvk_pass_end(render3d_st) + gvk_labels_close(render3d_st) gvk_once_end(render3d_st, render3d_st.gvk_cb) render3d_st.gvk_cb = null render3d_st.gvk_frame_no += 1 @@ -1669,6 +1680,7 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool { function gvk_present_window(render3d_st: mut Render3dState) -> void { let cb = gvk_frame_cb(render3d_st) gvk_pass_end(render3d_st) + gvk_labels_close(render3d_st) # a covered window is not drawn to: its layer would hold the frame for a drawable the compositor # hands back once a second. The frame still runs, paced at about 60 Hz, so the game keeps time. if win_visible() == 0 { diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index 298cb72c..4d25a78a 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -560,6 +560,9 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { if gvk_buf_busy(render3d_st, b) { # a draw recorded this frame (or the frame in flight) still reads it: destroy it once the frame has been submitted push(render3d_st.gvk_retired_buf, render3d_st.gvk_buf[b]); push(render3d_st.gvk_retired_mem, render3d_st.gvk_buf_mem[b]) + if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int } + while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) - 1 { push(render3d_st.gvk_retired_frame, 0) } + push(render3d_st.gvk_retired_frame, render3d_st.gvk_buf_used[b]) } else { Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b])) @@ -569,18 +572,32 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { # read by a draw recorded this frame, or by the presented frame still on the GPU function gvk_buf_busy(render3d_st: Render3dState, b: int) -> bool { let u = render3d_st.gvk_buf_used[b] - return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_no - 1) + return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_pending_no) } -# the frame's submitted work is done: storage retired during it can go -function gvk_retire_flush(render3d_st: mut Render3dState) -> void { +# everything submitted is done: every retired buffer can go +function gvk_retire_flush(render3d_st: mut Render3dState) -> void { gvk_retire_upto(render3d_st, render3d_st.gvk_frame_no) } + +# frame `done` and every frame before it are finished: storage last read by them can go, and what +# the frame being recorded read stays +function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void { if render3d_st.gvk_retired_buf == null { return } + let keep_buf = new []long + let keep_mem = new []long + let keep_frame = new []int for i in 0 .. len(render3d_st.gvk_retired_buf) { - Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null) - gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i])) + var f = 0 + if render3d_st.gvk_retired_frame != null and i < len(render3d_st.gvk_retired_frame) { f = render3d_st.gvk_retired_frame[i] } + if f > done { + push(keep_buf, render3d_st.gvk_retired_buf[i]); push(keep_mem, render3d_st.gvk_retired_mem[i]); push(keep_frame, f) + } else { + Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null) + gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i])) + } } - render3d_st.gvk_retired_buf = new []long - render3d_st.gvk_retired_mem = new []long + render3d_st.gvk_retired_buf = keep_buf + render3d_st.gvk_retired_mem = keep_mem + render3d_st.gvk_retired_frame = keep_frame } # A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there, diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index b1e2d321..88152c55 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -113,6 +113,8 @@ function grass_init(render3d_st: mut Render3dState) -> void { render3d_st.grass_radius = 1600.0 if r3d_env_has(render3d_st, "R3D_NOBLADES") { render3d_st.grass_on = false } if r3d_env_has(render3d_st, "R3D_GRASS_R") { render3d_st.grass_radius = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_R"))) } + if r3d_env_has(render3d_st, "R3D_GRASS_S0") { render3d_st.grass_s0 = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_S0"))) / 1000.0 } + if r3d_env_has(render3d_st, "R3D_GRASS_D0") { render3d_st.grass_d0 = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_D0"))) } if r3d_env_has(render3d_st, "R3D_GRASS_DBG") { render3d_st.grass_dbg = Text.to_int(r3d_env(render3d_st, "R3D_GRASS_DBG")) } } @@ -179,7 +181,7 @@ function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_ } function grass_draw(render3d_st: mut Render3dState) -> void { - if not render3d_st.grass_on or render3d_st.ter_reflect or render3d_st.grass_prog == 0 { return } + if not (render3d_st.grass_on or render3d_st.grass_force) or render3d_st.ter_reflect or render3d_st.grass_prog == 0 { return } var p = render3d_st.grass_prog if grass_mesh_live(render3d_st) { p = render3d_st.grass_mesh_prog } gpu_use_program(render3d_st, p) diff --git a/packages/ludic.render3d/prof.ludic b/packages/ludic.render3d/prof.ludic index 32bd1391..6fe9fc53 100644 --- a/packages/ludic.render3d/prof.ludic +++ b/packages/ludic.render3d/prof.ludic @@ -46,6 +46,7 @@ function prof_slot(render3d_st: mut Render3dState, name: pointer) -> int { function prof_begin(render3d_st: mut Render3dState, name: pointer) -> void { ds_set_pass(render3d_st, name) + if render3d_st.gvk_labels { gvk_label_begin(render3d_st, name) } if not render3d_st.prof_on { return } if render3d_st.prof_active >= 0 { return } # GL_TIME_ELAPSED queries cannot nest let s = prof_slot(render3d_st, name) @@ -56,6 +57,7 @@ function prof_begin(render3d_st: mut Render3dState, name: pointer) -> void { function prof_end(render3d_st: mut Render3dState) -> void { ds_set_pass(render3d_st, render3d_st.ds_outside) + if render3d_st.gvk_labels { gvk_label_end(render3d_st) } if not render3d_st.prof_on { return } if render3d_st.prof_active < 0 { return } gpu_query_end(render3d_st) diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 57c45364..0fcd601e 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -56,6 +56,8 @@ function r3d_env_flags(render3d_st: mut Render3dState) -> void { if r3d_env_has(render3d_st, "R3D_NOGI") { render3d_st.post_gi_strength = 0.0; render3d_st.post_ao_strength = 0.0; render3d_st.post_no_gi = true } if r3d_env_has(render3d_st, "R3D_MSAA") { render3d_st.post_ms_samples = Text.to_int(r3d_env(render3d_st, "R3D_MSAA")) } render3d_st.sc_skip_blade = r3d_env_has(render3d_st, "R3D_NOBLADES") + render3d_st.sc_skip_grass = r3d_env_has(render3d_st, "R3D_NOGRASS") + render3d_st.grass_force = r3d_env_has(render3d_st, "R3D_BLADES") render3d_st.sc_skip_card = r3d_env_has(render3d_st, "R3D_NOCARDS") render3d_st.sc_dbg_lod = r3d_env_has(render3d_st, "R3D_LODDBG") if r3d_env_has(render3d_st, "R3D_ANISO") { diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 6a4edf88..005549b3 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -36,6 +36,7 @@ property Layer { last_cam: floats, rough: float = 0.0, blade: bool = false, + grass: bool = false, # ground-cover grass the game may hand to the GPU blades (R3D_NOGRASS skips it) flower: bool = false, card: bool = false, cheap: bool = false, # distant cover: no shadows, no wind, flat lighting @@ -1134,6 +1135,7 @@ function scatter_draw_depth(render3d_st: mut Render3dState) -> void { for i in 0 .. len(render3d_st.sc_layers) { let l = render3d_st.sc_layers[i] if not l.foliage or l.blade or l.flower { continue } + if render3d_st.sc_skip_grass and l.grass { continue } if render3d_st.r3d_no_trees and l.imp != null { continue } layer_update(render3d_st, l) if l.n_lods > 1 and l.g_on { @@ -1155,6 +1157,7 @@ function scatter_draw(render3d_st: mut Render3dState) -> void { if render3d_st.sc_skip_blade and l.blade { continue } if render3d_st.sc_skip_flower and l.flower { continue } if render3d_st.sc_skip_card and l.card { continue } + if render3d_st.sc_skip_grass and l.grass { continue } if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue } layer_update(render3d_st, l) layer_draw_near(render3d_st, l, false, null, false) @@ -1172,6 +1175,7 @@ function scatter_draw_casters(render3d_st: mut Render3dState, light_vp: floats) let l = render3d_st.sc_layers[i] if render3d_st.sc_skip_blade and l.blade { continue } if render3d_st.sc_skip_card and l.card { continue } + if render3d_st.sc_skip_grass and l.grass { continue } if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue } layer_update(render3d_st, l) if l.imp != null {