feat(render3d): mesh-shader grass

The chunked grass path draws each chunk as one mesh-shader dispatch when the
setting asks and the card has VK_EXT_mesh_shader: work group y is a tile, x a
batch of 16 of its blades, and a blade the placement, density, frustum, water
or slope tests reject emits nothing - the instanced path still runs its eight
vertices to a degenerate position. grass.mesh generates the same blades as
grass.vert (grass_blade_mesh(4)'s rows, the same hashes, sway and lighting
normal), capped at the instanced path's 65535 a tile.

Vulkan: VK_EXT_mesh_shader with meshShader, and maintenance4 (glslang's mesh
stages declare LocalSizeId); vkCmdDrawMeshTasksEXT looked up per device, as the
Streamline interposer exports none; a *.mesh program's pipeline takes the mesh
stage and no vertex input, its bindings the mesh stage bit. gpu_has_mesh,
gpu_draw_mesh_tasks; r3d_mesh_grass and R3D_MESH_GRASS / R3D_NO_MESH.
bin/ludic-dev rebuilt: the committed binary predated the shader tool's mesh
support and compiled grass.mesh as a vertex stage.

PC (RTX 3070 Ti): the camp matches the chunked path; validation only the
no-window present-id message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 20:20:42 +03:00
parent 6525c11b3c
commit 750d13779d
10 changed files with 334 additions and 13 deletions

View file

@ -35,6 +35,17 @@ var grass_cmds: int = 0
var grass_band_start: words = null # per band: its first record
var grass_band_cells: words = null # ... and its 16 m cells per tile side
var grass_band_n: int = 0
# Mesh-shader grass (Settings, Video, Advanced): the chunked path's records, but each chunk is one
# mesh dispatch - work group y a tile, x a batch of GRASS_MESH_BLADES of its blades - so a blade the
# tests reject emits nothing instead of eight degenerate vertices. R3D_MESH_GRASS=1 / 0 overrides.
const GRASS_MESH_BLADES: int = 16
var grass_mesh_on: bool = false
var grass_mesh_prog: int = 0
function r3d_mesh_grass(on: bool) -> void {
grass_mesh_on = on
if Os.has_env("R3D_MESH_GRASS") { grass_mesh_on = Text.to_int(Os.env("R3D_MESH_GRASS")) != 0 }
}
function grass_mesh_live() -> bool { return grass_mesh_on and grass_merge and grass_mesh_prog != 0 and gpu_has_mesh() }
# a blade: `rows` rows of 2 vertices (x across, y along, z bend), attribute 2 = uv
function grass_blade_mesh(rows: int) -> Mesh {
@ -81,6 +92,7 @@ function grass_init() -> void {
grass_cmds = gpu_buffer_new()
}
grass_prog = r3d_program("grass.vert", "model.frag", defs)
if grass_merge and gpu_has_mesh() { grass_mesh_prog = r3d_program("grass.mesh", "model.frag", "#define FOLIAGE\n#define BLADE\n#define MESH\n") }
grass_mesh = grass_blade_mesh(4)
grass_wind = fl(2.4)
grass_s0 = fl(0.11)
@ -155,7 +167,8 @@ function grass_tiles(size: int, d_min: int, d_max: int) -> void {
function grass_draw() -> void {
if not grass_on or ter_reflect or grass_prog == 0 { return }
let p = grass_prog
var p = grass_prog
if grass_mesh_live() { p = grass_mesh_prog }
gpu_use_program(p)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
@ -203,8 +216,9 @@ function grass_draw() -> void {
# the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw
function grass_flush() -> void {
if grass_n == 0 { return }
let p = grass_prog
gpu_buffer_upload(grass_cmds, grass_n * 20, grass_rec, GPU_DYNAMIC)
var p = grass_prog
let mesh = grass_mesh_live()
if mesh { p = grass_mesh_prog } else { gpu_buffer_upload(grass_cmds, grass_n * 20, grass_rec, GPU_DYNAMIC) }
for b in 0 .. grass_band_n {
let s = grass_band_start[b]
var e = grass_n
@ -216,7 +230,16 @@ function grass_flush() -> void {
if m > GRASS_CHUNK { m = GRASS_CHUNK }
for q in 0 .. m * 4 { grass_chunk_tv[q] = grass_tv[k * 4 + q] }
u_f4v(gpu_uniform(p, "u_tiles"), m, grass_chunk_tv)
gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0)
if mesh {
# enough blade batches for the chunk's largest tile, capped as the instanced path caps a tile
let cells = grass_band_cells[b]
var most = 0
for q in 0 .. m { let t = f_to_int(grass_tv[(k + q) * 4 + 2]) * cells * cells; if t > most { most = t } }
if most > 65535 { most = 65535 }
gpu_draw_mesh_tasks((most + GRASS_MESH_BLADES - 1) / GRASS_MESH_BLADES, m, 1)
} else {
gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0)
}
grass_draws += 1
k += m
}