perf(render3d): Vulkan draws find pipelines by integer and reuse descriptor sets
R3D_VK_PROF at the camp view (about 2950 draws a frame) showed the frame spent in bookkeeping: 19.5 ms a frame finding pipelines by string key and 16 ms building descriptor sets. - Pipelines: a mesh carries an interned vertex-layout id (dropped only when an attribute's shape changes, not when an instance buffer is swapped), render state packs into an int and the pass formats into another; the program's last hit is tried first. The string path only builds. - Samplers: each texture keeps the sampler for its parameters until they change. - Descriptor sets: a program's last set is reused within the frame while its blocks and resolved textures are unchanged; a uniform write that repeats the value it already holds changes nothing. Headless on the RTX 3070 Ti at 1920x1080: 21.7 -> 53.3 fps (pipelines 0.2 ms, sets 8.5 ms, inside draws 10 ms a frame; OpenGL 114 fps). The camp frame is unchanged and validation-clean. OpenGL frames byte-identical at the five viewpoints; 59 self-tests pass; VKRES OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6942c36853
commit
f5d1a62ccf
4 changed files with 132 additions and 8 deletions
|
|
@ -448,6 +448,8 @@ function gpu_mesh_record(m: Mesh, index: int, comps: int, type: int, stride: int
|
|||
let o = index * GPU_ATTR_W
|
||||
var st = stride
|
||||
if st == 0 { st = comps * gpu_type_bytes(type) }
|
||||
# re-pointing an attribute at another buffer keeps the layout; changing its shape does not
|
||||
if m.attrs[o + 1] != comps or m.attrs[o + 2] != type or m.attrs[o + 3] != st or m.attrs[o + 4] != offset or m.attrs[o + 5] != gpu_b(normalized) or m.attrs[o + 6] != gpu_b(inst) or (m.attrs[o] == m.attrs[0]) != (m.cur_buf == m.attrs[0]) { m.vk_layout = 0 }
|
||||
m.attrs[o] = m.cur_buf; m.attrs[o + 1] = comps; m.attrs[o + 2] = type; m.attrs[o + 3] = st
|
||||
m.attrs[o + 4] = offset; m.attrs[o + 5] = gpu_b(normalized); m.attrs[o + 6] = gpu_b(inst)
|
||||
if index + 1 > m.n_attrs { m.n_attrs = index + 1 }
|
||||
|
|
|
|||
|
|
@ -371,10 +371,27 @@ var gvk_ublk_v: []bytes = null # per program: the vertex stage's block as
|
|||
var gvk_ublk_f: []bytes = null
|
||||
var gvk_prog_tex: []words = null # per program: the texture bound to each manifest sampler
|
||||
var gvk_prog_unit: []words = null # per program: the unit + 1 each sampler was pointed at with u_i
|
||||
var gvk_prog_dirty: []int = null # per program: its blocks or textures changed since its last set
|
||||
var gvk_set_last: []long = null # per program: the descriptor set its last draw used
|
||||
var gvk_set_frame: []int = null # per program: the frame that set belongs to
|
||||
var gvk_set_tex: []words = null # per program: the textures that set was filled with
|
||||
|
||||
function gvk_same(a: pointer, b: pointer, n: int) -> bool {
|
||||
var i = 0
|
||||
while i < n { if Vk.get_i32(a, i) != Vk.get_i32(b, i) { return false }; i += 4 }
|
||||
return true
|
||||
}
|
||||
|
||||
function gvk_uniform_blocks(p: int) -> void {
|
||||
if gvk_ublk_v == null { gvk_ublk_v = new []bytes; gvk_ublk_f = new []bytes; gvk_prog_tex = new []words; gvk_prog_unit = new []words }
|
||||
while len(gvk_ublk_v) <= p { push(gvk_ublk_v, null); push(gvk_ublk_f, null); push(gvk_prog_tex, null); push(gvk_prog_unit, null) }
|
||||
if gvk_ublk_v == null {
|
||||
gvk_ublk_v = new []bytes; gvk_ublk_f = new []bytes; gvk_prog_tex = new []words; gvk_prog_unit = new []words
|
||||
gvk_prog_dirty = new []int; gvk_set_last = new []long; gvk_set_frame = new []int; gvk_set_tex = new []words
|
||||
}
|
||||
let zero: long = 0
|
||||
while len(gvk_ublk_v) <= p {
|
||||
push(gvk_ublk_v, null); push(gvk_ublk_f, null); push(gvk_prog_tex, null); push(gvk_prog_unit, null)
|
||||
push(gvk_prog_dirty, 1); push(gvk_set_last, zero); push(gvk_set_frame, 0); push(gvk_set_tex, null)
|
||||
}
|
||||
let v = gvk_prog_var[p]
|
||||
if v == null or gvk_ublk_v[p] != null or gvk_prog_tex[p] != null { return }
|
||||
if v.vblock > 0 { let b = bytes(v.vblock); Vk.zero(b, v.vblock); gvk_ublk_v[p] = b }
|
||||
|
|
@ -385,6 +402,9 @@ function gvk_uniform_blocks(p: int) -> void {
|
|||
for i in 0 .. nt { t[i] = 0; u[i] = 0 }
|
||||
gvk_prog_tex[p] = t
|
||||
gvk_prog_unit[p] = u
|
||||
let st = words(nt)
|
||||
for i in 0 .. nt { st[i] = -1 }
|
||||
gvk_set_tex[p] = st
|
||||
}
|
||||
|
||||
function gvk_uniform(p: int, name: string) -> int {
|
||||
|
|
@ -407,7 +427,7 @@ function gvk_u_set(loc: int, src: pointer, size: int, n: int) -> void {
|
|||
gvk_uniform_blocks(p)
|
||||
if loc % 4096 >= 2048 {
|
||||
let si = loc % 4096 - 2048
|
||||
if si < len(v.t_name) { gvk_prog_unit[p][si] = Vk.get_i32(src, 0) + 1 }
|
||||
if si < len(v.t_name) and gvk_prog_unit[p][si] != Vk.get_i32(src, 0) + 1 { gvk_prog_unit[p][si] = Vk.get_i32(src, 0) + 1; gvk_prog_dirty[p] = 1 }
|
||||
return
|
||||
}
|
||||
let name = v.u_name[loc % 4096]
|
||||
|
|
@ -423,7 +443,11 @@ function gvk_u_set(loc: int, src: pointer, size: int, n: int) -> void {
|
|||
if count > v.u_count[j] { count = v.u_count[j] }
|
||||
for k in 0 .. count {
|
||||
let at = v.u_off[j] + k * stride
|
||||
if at + size <= cap { mem_copy(mem_off(blk, at), mem_off(src, k * size), size) }
|
||||
# the same value again (most per-draw uniforms repeat) leaves the program's set reusable
|
||||
if at + size <= cap and not gvk_same(mem_off(blk, at), mem_off(src, k * size), size) {
|
||||
mem_copy(mem_off(blk, at), mem_off(src, k * size), size)
|
||||
gvk_prog_dirty[p] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -434,7 +458,7 @@ function gvk_bind_texture(p: int, name: string, tex: int) -> void {
|
|||
let v = gvk_prog_var[p]
|
||||
if v == null { return }
|
||||
gvk_uniform_blocks(p)
|
||||
for i in 0 .. len(v.t_name) { if v.t_name[i] == name { gvk_prog_tex[p][i] = tex } }
|
||||
for i in 0 .. len(v.t_name) { if v.t_name[i] == name and gvk_prog_tex[p][i] != tex { gvk_prog_tex[p][i] = tex; gvk_prog_dirty[p] = 1 } }
|
||||
}
|
||||
|
||||
# ---- per-frame uniform ring and descriptor sets -------------------------------------------
|
||||
|
|
@ -497,6 +521,16 @@ function gvk_draw_set(p: int, tx: words, tx_w: int, tx_cap: int) -> long {
|
|||
let zero: long = 0
|
||||
let v = gvk_prog_var[p]
|
||||
gvk_uniform_blocks(p)
|
||||
let nt0 = len(v.t_name)
|
||||
# the textures this draw reads, resolved the way the set below binds them
|
||||
var same_tex = true
|
||||
for t in 0 .. nt0 {
|
||||
var tt = gvk_prog_tex[p][t]
|
||||
let u1 = gvk_prog_unit[p][t]
|
||||
if tt <= 0 and u1 > 0 and u1 <= 32 and gpu_unit_2d != null { tt = gpu_unit_2d[u1 - 1] }
|
||||
if gvk_set_tex[p][t] != tt { same_tex = false; gvk_set_tex[p][t] = tt }
|
||||
}
|
||||
if same_tex and gvk_prog_dirty[p] == 0 and gvk_set_frame[p] == gvk_frame_no and gvk_set_last[p] != 0 { return gvk_set_last[p] }
|
||||
let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof)
|
||||
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
|
||||
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
|
||||
|
|
@ -548,7 +582,7 @@ function gvk_draw_set(p: int, tx: words, tx_w: int, tx_cap: int) -> long {
|
|||
var smp: long = 0
|
||||
let o = tex * tx_w
|
||||
if tex != gvk_white and tex < tx_cap {
|
||||
smp = gvk_sampler(tx[o + 5], tx[o + 6], tx[o + 7], tx[o + 8], tx[o + 9], tx[o + 11])
|
||||
smp = gvk_tex_sampler(tex, tx[o + 5], tx[o + 6], tx[o + 7], tx[o + 8], tx[o + 9], tx[o + 11])
|
||||
} else {
|
||||
smp = gvk_sampler(GL_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 0)
|
||||
}
|
||||
|
|
@ -564,6 +598,9 @@ function gvk_draw_set(p: int, tx: words, tx_w: int, tx_cap: int) -> long {
|
|||
nw += 1
|
||||
}
|
||||
if nw > 0 { Vk.update_descriptor_sets(gvk_dev, nw, writes, 0, null) }
|
||||
gvk_set_last[p] = set
|
||||
gvk_set_frame[p] = gvk_frame_no
|
||||
gvk_prog_dirty[p] = 0
|
||||
return set
|
||||
}
|
||||
|
||||
|
|
@ -832,7 +869,7 @@ function gvk_draw(p: int, m: Mesh, st: GvkState, first: int, count: int, instanc
|
|||
gvk_pass_begin(rec, rec_o)
|
||||
let cb = gvk_cb
|
||||
let samples = 1
|
||||
let pipe = gvk_pipeline(p, m, st, gvk_pass_ncolor, gvk_pass_cfmt, gvk_pass_dfmt, samples)
|
||||
let pipe = gvk_pipeline_fast(p, m, st, gvk_pass_ncolor, gvk_pass_cfmt, gvk_pass_dfmt, samples)
|
||||
if pipe == 0 { return }
|
||||
var t1: long = 0
|
||||
if prof { t1 = gl_now_us(); gvk_us_pipe = gvk_us_pipe + (t1 - t0) }
|
||||
|
|
@ -1305,3 +1342,69 @@ function gvk_prof_frame() -> void {
|
|||
gvk_us_pipe = zero; gvk_us_set = zero; gvk_us_draw = zero
|
||||
gvk_n_draws = 0; gvk_n_flush = 0; gvk_prof_frames = 0
|
||||
}
|
||||
|
||||
# ---- the pipeline cache, by integers -----------------------------------------------------------
|
||||
# gvk_pipeline builds and keys pipelines by a string; a draw only needs to find one it already has.
|
||||
# A mesh carries an interned layout id (its recorded layout, buffers named by order), the render
|
||||
# state packs into one int, and the pass's formats into another; the program's last hit is tried
|
||||
# first, then the entries it owns.
|
||||
var gvk_layout_keys: []string = null
|
||||
var gvk_pc_prog: []int = null
|
||||
var gvk_pc_layout: []int = null
|
||||
var gvk_pc_state: []int = null
|
||||
var gvk_pc_bias: []int = null
|
||||
var gvk_pc_pass: []int = null
|
||||
var gvk_pc_pipe: []long = null
|
||||
var gvk_pc_last: words = null
|
||||
|
||||
function gvk_layout_id(m: Mesh) -> int {
|
||||
if m == null or m.attrs == null { return 1 }
|
||||
if m.vk_layout > 0 { return m.vk_layout }
|
||||
let key = gvk_layout_key(m)
|
||||
if gvk_layout_keys == null { gvk_layout_keys = new []string }
|
||||
var id = 0
|
||||
for i in 0 .. len(gvk_layout_keys) { if id == 0 and gvk_layout_keys[i] == key { id = i + 2 } }
|
||||
if id == 0 { push(gvk_layout_keys, key); id = len(gvk_layout_keys) + 1 }
|
||||
m.vk_layout = id
|
||||
return id
|
||||
}
|
||||
function gvk_blend_index(f: int) -> int {
|
||||
if f == GL_ZERO { return 0 }
|
||||
if f == GL_ONE { return 1 }
|
||||
if f == GL_SRC_ALPHA { return 2 }
|
||||
if f == GL_ONE_MINUS_SRC_ALPHA { return 3 }
|
||||
if f == GL_DST_ALPHA { return 4 }
|
||||
if f == GL_ONE_MINUS_DST_ALPHA { return 5 }
|
||||
if f == GL_SRC_COLOR { return 6 }
|
||||
if f == GL_ONE_MINUS_SRC_COLOR { return 7 }
|
||||
return 8
|
||||
}
|
||||
function gvk_pipeline_fast(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long {
|
||||
let layout = gvk_layout_id(m)
|
||||
var state = st.depth_test | (st.depth_write << 1) | (st.blend << 2) | (st.cull << 3) | (st.color_write << 4) | (st.a2c << 5) | (st.bias << 6) | (st.wireframe << 7)
|
||||
state = state | ((st.depth_func & 15) << 8) | ((st.cull_face & 15) << 12) | (gvk_blend_index(st.blend_src) << 16) | (gvk_blend_index(st.blend_dst) << 20)
|
||||
let bias = st.bias_factor * 31 + st.bias_units
|
||||
let pass = ((n_color * 1000 + color_fmt) * 1000 + depth_fmt) * 8 + samples
|
||||
if gvk_pc_prog == null {
|
||||
gvk_pc_prog = new []int; gvk_pc_layout = new []int; gvk_pc_state = new []int; gvk_pc_bias = new []int
|
||||
gvk_pc_pass = new []int; gvk_pc_pipe = new []long
|
||||
gvk_pc_last = words(4096)
|
||||
for i in 0 .. 4096 { gvk_pc_last[i] = -1 }
|
||||
}
|
||||
if p < 4096 {
|
||||
let k = gvk_pc_last[p]
|
||||
if k >= 0 and gvk_pc_layout[k] == layout and gvk_pc_state[k] == state and gvk_pc_pass[k] == pass and gvk_pc_bias[k] == bias { return gvk_pc_pipe[k] }
|
||||
}
|
||||
for k in 0 .. len(gvk_pc_prog) {
|
||||
if gvk_pc_prog[k] == p and gvk_pc_layout[k] == layout and gvk_pc_state[k] == state and gvk_pc_pass[k] == pass and gvk_pc_bias[k] == bias {
|
||||
if p < 4096 { gvk_pc_last[p] = k }
|
||||
return gvk_pc_pipe[k]
|
||||
}
|
||||
}
|
||||
let pipe = gvk_pipeline(p, m, st, n_color, color_fmt, depth_fmt, samples)
|
||||
if pipe == 0 { return pipe }
|
||||
push(gvk_pc_prog, p); push(gvk_pc_layout, layout); push(gvk_pc_state, state)
|
||||
push(gvk_pc_bias, bias); push(gvk_pc_pass, pass); push(gvk_pc_pipe, pipe)
|
||||
if p < 4096 { gvk_pc_last[p] = len(gvk_pc_prog) - 1 }
|
||||
return pipe
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ 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_tex_smp_sig: []int = null # the sampling parameters the cached sampler was made for
|
||||
var gvk_tex_smp: []long = null
|
||||
var gvk_unpack_swap: bool = false # GL_UNPACK_SWAP_BYTES: 16-bit PNG samples arrive big-endian
|
||||
|
||||
function gvk_tex_new() -> int {
|
||||
|
|
@ -64,8 +66,10 @@ function gvk_tex_new() -> int {
|
|||
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
|
||||
gvk_tex_smp_sig = new []int; gvk_tex_smp = new []long
|
||||
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)
|
||||
push(gvk_tex_smp_sig, 0); push(gvk_tex_smp, zero)
|
||||
# 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)
|
||||
|
|
@ -74,6 +78,7 @@ function gvk_tex_new() -> int {
|
|||
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)
|
||||
push(gvk_tex_smp_sig, 0); push(gvk_tex_smp, zero)
|
||||
return len(gvk_tex_image) - 1
|
||||
}
|
||||
|
||||
|
|
@ -446,6 +451,19 @@ function gvk_compare_op(f: int) -> int {
|
|||
if f == GL_ALWAYS { return VK_COMPARE_OP_ALWAYS }
|
||||
return VK_COMPARE_OP_LESS_OR_EQUAL
|
||||
}
|
||||
# the sampler for texture tex's parameters, from the texture's own one-entry cache when they have
|
||||
# not changed since it last asked - a draw asks for every texture it binds
|
||||
function gvk_tex_sampler(tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
|
||||
var sig = min_f * 31 + mag_f
|
||||
sig = sig * 31 + wrap_s
|
||||
sig = sig * 31 + wrap_t
|
||||
sig = sig * 31 + compare
|
||||
sig = (sig * 31 + aniso) | 1
|
||||
if tex > 0 and tex < len(gvk_tex_smp_sig) and gvk_tex_smp_sig[tex] == sig { return gvk_tex_smp[tex] }
|
||||
let s = gvk_sampler(min_f, mag_f, wrap_s, wrap_t, compare, aniso)
|
||||
if tex > 0 and tex < len(gvk_tex_smp_sig) { gvk_tex_smp_sig[tex] = sig; gvk_tex_smp[tex] = s }
|
||||
return s
|
||||
}
|
||||
function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
|
||||
let key = `{min_f}/{mag_f}/{wrap_s}/{wrap_t}/{compare}/{aniso}`
|
||||
if gvk_smp_keys == null { gvk_smp_keys = new []string; gvk_smp = new []long }
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ property Mesh {
|
|||
vbufs: words, # every vertex buffer the mesh owns, freed with it
|
||||
n_vbufs: int = 0,
|
||||
cur_buf: int = 0, # the buffer the next attribute reads (while building)
|
||||
ibuf: int = 0 # the instance buffer attached last
|
||||
ibuf: int = 0, # the instance buffer attached last
|
||||
vk_layout: int = 0 # the Vulkan backend's interned id for the recorded layout (0: not yet)
|
||||
}
|
||||
|
||||
function mesh_draw(m: Mesh) -> void { gpu_draw_mesh(m) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue