Commit graph

9 commits

Author SHA1 Message Date
750d13779d 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>
2026-09-15 20:20:42 +03:00
6525c11b3c feat(render3d): HDR10 output, and DLSS that stays still
HDR output: an HDR10 swapchain (A2B10G10R10, ST 2084 over BT.2020) when the
setting asks and the display offers it, with HDR metadata. The tonemap's HDR10
variant keeps the SDR picture up to a 200-nit paper white and rolls highlights
on to 1000 nits; the overlay's converts the interface to the same white. The
screen and LDR images go 10-bit with it; screenshots refuse while it is on.
OpenGL and the Vulkan SDR frame are unchanged. The instance asks for
VK_EXT_swapchain_colorspace. HDR metadata only where the loader has
vkSetHdrMetadataEXT: Streamline's interposer does not, and calling the thunk
crashed the game the moment the swapchain came up HDR10. PC 4K monitor: HDR10,
validation 0. R3D_HDR overrides the setting.

DLSS:
- the vertical jitter offset flips with Streamline's image (rows from the top):
  unflipped, Quality resolved the ground into concentric rings;
- preset K in every mode: the default M put Performance at 18 ms a frame at 4K
  on an RTX 3070 Ti (33 fps against 41 with DLSS off; with K, 60);
- the camera is jittered only while this frame holds a token and the last
  evaluate worked.
R3D_DLSS_PRESET, R3D_CAM_LOG (the camera and DLSS state a frame) and
R3D_NOGRAIN for measuring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:06:18 +03:00
ac539b0150 feat(render3d): DLSS super resolution and Reflex through NVIDIA Streamline
streamline.ludic: slInit before the Vulkan instance, feature support per
adapter, a frame token per frame, Reflex sleep and PCL latency markers, and
DLSS super resolution on the lit HDR frame (Halton jitter, depth + zero
motion vectors with camera motion from clipToPrevClip, matrices carrying
the Vulkan path's y flip and depth remap). Bloom, tonemap and sharpen read
the upscaled size. The device asks for privateData and present_id, which
Streamline's hooks need. R3D_DLSS / R3D_REFLEX / R3D_SL_LOG for tests.
gpu_feature_implemented: DLSS and Reflex.

ludic bundle (Windows): app native "<dir>" copies native libraries beside
the executable.

Verified on an RTX 3070 Ti: DLSS Quality evaluates 1280x720 -> 1920x1080.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:26:12 +03:00
5b9403c343 render3d: actors cast only into the cascades they can shade; Vulkan GPU timings and a mean frame split in R3D_PROF
Where the frame goes, before optimising it further. R3D_PROF now prints the average frame: CPU before
the swap (the game's share apart), GPU and swap, and the renderer's CPU phases in frame order - the
shadow pass split into cascade fit, scatter casters and actor casters. On Vulkan the per-pass GPU
table comes from timestamp queries (host query reset asked for where the device has it); MoltenVK's
attribution is tile-based and not to be trusted per pass, the PC's is.

What it showed on the PC (camp): 4.3 ms CPU and 5.3 ms GPU a frame; the shadow pass was the largest
CPU phase (1.8 ms) and actors half of that. Every actor within 300 m was drawn into all five cascades,
though the outer two only shade receivers from 212 and 935 m out: 160 actors and 300 draws into each.
cast_band_reaches - the flowers' reach test, now shared - skips an actor for a cascade it cannot shade
(receivers counted from 0.85 of the previous split, where sunShadow's cross-fade begins).

PC camp, two runs each: mean frame 9553/9601 -> 8664/8656 us; CPU 4.3 -> 3.7 ms; shadow GPU 1.14 ->
0.79 ms; actor-shadow CPU 1.02 -> 0.60 ms; 2039 -> 1411 draws; self-tests 59/59, validation 0.
Mac: OpenGL shot viewpoints and the camp byte-identical; town 19 px at <= 2/255 on two flower stems a
few metres from the camera - the accepted leftover-binding difference, no shadow; self-tests 59/59 on
OpenGL and Vulkan. R3D_CAST_ALL=1 draws every caster into every cascade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:03:31 +03:00
989bdca736 perf(render3d): Vulkan device memory in blocks, and a shadow map that survives running out of it
- A block sub-allocator: 64 MB blocks per memory type, images and buffers kept apart, first fit with
  alignment, freed ranges merged and empty blocks given back; anything over 16 MB still gets its own
  allocation. The self-tests' second world holds 94 allocations instead of 8735 (the driver's limit
  refused a shadow map before). Camp bench unchanged, 105.3 fps.
- shadow_set_res keeps the size that worked when the card has no memory for the new one, and records
  it in shadow_refused, instead of ending with no shadow map; gpu_tex_ok says whether a texture has an
  image behind it.
- Image barriers skip an image that was never made (a failed allocation used to crash there), and
  R3D_VK_ERRLOG=<file> appends every Vulkan failure line by line, so a crash no longer takes the
  message with it.
- R3D_VK_PROF reports draws asked for and not made, so a layer missing from a frame is never silent.

Validation on (VK_INSTANCE_LAYERS): the game's self-tests 61 OK, 0 errors, on the RTX 3070 Ti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 15:44:21 +03:00
c1eb5f399f feat(render3d): compute and indirect draws on Vulkan, and tree layers culled on the GPU (opt-in)
- Device: multiDrawIndirect, drawIndirectFirstInstance and drawIndirectCount where present.
- Buffers carry storage and indirect usage; a GPU-owned buffer is never swapped under a draw.
- Compute programs from shaders/compute.list (binding 0 parameters, 1.. storage buffers),
  built by `ludic-dev shaders`; gpu_compute / gpu_dispatch / gpu_draw_mesh_indirect in gpu.ludic.
- R3D_VK_PROBE=1: a dispatch read back (OK on the RTX 3070 Ti).
- scatter_cull.comp: a tree layer's frustum test and LOD split on the GPU, with the lit, prepass,
  impostor and shadow-LOD draws reading its records. Behind R3D_GPU_CULL=1 and off by default:
  at the camp it is slower (43.0 fps against 53.3), because the frame's cost is per-draw
  descriptor sets and it adds empty-level draws. Validation-clean; OpenGL frames unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 14:19:43 +03:00
1b7408c6a1 feat(render3d): Vulkan in the game's window on Windows
- gpu_select takes Vulkan for a window on Windows; gvk_init asks for VK_KHR_surface,
  VK_KHR_win32_surface and VK_KHR_swapchain when the renderer will present. A window elsewhere
  stays on OpenGL with the reason.
- gvk_open opens the window without a GL context and makes the screen images at its client area;
  gvk_swap_make builds the swapchain on the Win32 surface (FIFO with vsync, mailbox or immediate
  without) and rebuilds it when it is out of date, suboptimal or the window changes size.
- gvk_present blits the screen image into the acquired swapchain image, flipped (the screen keeps
  OpenGL's bottom-up rows), and presents it.
- Samplers pointed at a texture unit with u_i and then fed by binding units - the actors do this -
  read that unit's texture; on Vulkan they drew with the white stand-in (the tent, the log, the
  chair, the workbench).
- gl.ll carries a weak win_gl_drawable for headless macOS builds.

On the RTX 3070 Ti the windowed build runs the valley through Vulkan at 3840x2160 with the HUD and
the frame-rate readout (16 fps: every upload still waits on the frame, and buffers are
host-visible). The headless Vulkan frame is unchanged; OpenGL frames byte-identical at the five
viewpoints with 59 self-tests passing. The actor fix is compiled and OpenGL-verified, not yet
seen on the PC; blades at the grass's middle distance still draw black on Vulkan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:08:40 +03:00
c660ac881c feat(render3d): Vulkan textures, samplers and buffers
gpu_vk_res.ludic gains what gpu.ludic's texture and buffer handles stand for on Vulkan:

- gvk_tex_storage / _upload / _mips / _read / _release: an image and view per handle, a full mip
  chain when pixels come with it (the renderer asks for mipmaps after the upload) and one level
  for a target, every level in SHADER_READ_ONLY between uses. Uploads are converted to what the
  image stores: a missing alpha filled opaque (three-channel formats are stored with four), 16-bit
  PNG samples byte-swapped when the unpack state says so, 32-bit float HDR halved into half
  floats. Mips are blitted down level by level; a read-back brings level 0 home.
- gvk_sampler: one VkSampler per filter / wrap / compare / anisotropy combination, made when first
  asked for, with GL's defaults where the renderer set nothing.
- gvk_buf_*: vertex, index and instance buffers behind one handle, kept when an upload fits.
  Host-visible while the backend comes up.

gpu_vk.ludic switches on anisotropic sampling where the device has it and reads its limit.

examples/rendering/vk_resources.ludic checks it all: VKRES OK, validation-clean, every allocation
freed, on the RTX 3070 Ti (16x anisotropy) and on MoltenVK. One run on the Mac crashed while a
headless game run was using the GPU and did not come back in two reruns. OpenGL frames
byte-identical at the five viewpoints; 59 self-tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:57:28 +03:00
7d93f44e53 feat(render3d): gpu_vk.ludic - the Vulkan backend's device, memory and one-shot commands
The first part of the Vulkan side of gpu.ludic, not yet reached from the renderer: gvk_init
brings up the instance (portability enumeration where offered), the first discrete GPU, a
graphics queue and a device with the Tier 1 floor switched on (dynamic rendering,
synchronization2, descriptor indexing, timeline semaphores), and says why when it cannot so
the caller stays on OpenGL. gvk_mem_type / gvk_alloc pick and allocate memory (one allocation
per resource while the backend comes up), and gvk_once_begin / gvk_once_end carry uploads,
bakes and read-backs through submit and wait.

examples/rendering/vk_device.ludic runs it alone: VKDEVICE OK and validation-clean on the RTX
3070 Ti and on MoltenVK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:46:50 +03:00