Commit graph

10 commits

Author SHA1 Message Date
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
66940f9c0a fix(shaders): varyings meet by name on Vulkan, so the meadow's flowers draw
OpenGL links a vertex output to a fragment input by name; SPIR-V links them by location, and
glslang's --auto-map-locations numbered each stage in its own declaration order. The foliage
prepass's depth.frag declares v_wpos then v_uv where model.vert writes v_wpos, v_nrm, v_uv, so
the prepass read a normal as its texture coordinate, its alpha test cut every flower head and
leaf, and the lit pass (depth EQUAL) drew nothing over them. `ludic-dev shaders` now gives both
stages explicit locations: the vertex stage's out order numbers them and the fragment stage looks
each in up by name. All 45 variants checked: every fragment input sits on its vertex output.

Also:
- A clear still waiting for its pass when the framebuffer changes now runs on that framebuffer,
  instead of becoming the load op of whichever pass began next.
- R3D_DUMP_ATLAS writes every impostor and card atlas a run bakes (build/atlas_<n>_*.ppm); the
  40 baked on Vulkan match OpenGL's.

The PC's Vulkan frame now shows the flowers as OpenGL does, validation-clean. ludic-dev test 140
passed; OpenGL frames byte-identical at the five viewpoints; 59 self-tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:50:54 +03:00
ce3986bb02 refactor(render3d): programs, GPU timers and the context behind gpu.ludic
The last of milestone 1: no OpenGL call is left in render3d outside gpu.ludic but the clock
and the CPU-side buffer helpers.

- gpu_program builds a program and remembers the variant it came from (vertex, fragment and
  defines as one line - the SPIR-V manifest's key), so a backend that cannot compile at run
  time finds the pipeline for the same handle. gpu_use_program and gpu_program_free replace
  32 uses and 2 frees; the overlay's program is recorded under overlay.vert|overlay.frag.
- gpu_query_new / _begin / _end / _result carry R3D_PROF's timers.
- gpu_open, gpu_vsync, gpu_renderer_name and gpu_resize_check carry the context.
- r3d_program_tess is gone: nothing called it and no tessellation shader exists.
- R3D_GLCHECK also checks after framebuffer and renderbuffer changes, viewport, draw buffers,
  program use, texture parameters, the resize check and between frames.

OpenGL frames are byte-identical at the five viewpoints; 59 self-tests pass with and without
R3D_GLCHECK, with no GL error; ludic-dev test 140 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:33:01 +03:00
78c1f4f9ca refactor(render3d): render targets and passes behind gpu.ludic
Framebuffers and their attachments, renderbuffers (multisampled too), draw and read buffers,
completeness checks, blits, viewports, clears, the screen framebuffer, the multisample enable,
the wireframe switch and GL error checks now go through gpu_fb_* / gpu_rb_* / gpu_viewport /
gpu_clear / gpu_blit / gpu_check, and no other file names them. Each call is the one GL call it
replaces, in the same order: the fixed viewpoints render bit-identically and the game's
self-tests report exactly what they did.

What is attached to each framebuffer - colour slots, a depth texture or one layer of an array,
renderbuffers and their samples - is recorded as it is attached, for a backend that builds
render passes and image views. gpu_read_screen is the frame read-back a photograph takes.

R3D_GLCHECK=1 checks, around every draw, clear and blit, that the bound framebuffer is complete
and that no error is left behind, naming the framebuffer. It has already narrowed the old
"gl error 1286 at terrain shadow bake": the error is pending before a draw after the map self-
test, so it comes from a call that is not a draw, clear or blit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:41:43 +03:00
4d3479d431 refactor(render3d): textures behind gpu.ludic, their sampler state recorded
Texture creation, 2D and array uploads, pixel packing, filters, wraps, comparison, border,
anisotropy, mipmaps, texture units, read-backs and freeing now go through gpu_tex_* and
gpu_bind_sampler, and no other file names them. Each call is the one GL call it replaces,
in the same order, so OpenGL renders bit-identically at the fixed viewpoints and the game's
self-tests report what they did before.

What those calls say about a texture - size, format, layers, min and mag filter, wraps,
comparison, mipmaps, anisotropy - is recorded per handle as it is set: a backend with
immutable images and separate sampler objects creates both from exactly that.
r3d_bind_tex takes a texture kind (GPU_TEX2D / GPU_TEX2D_ARRAY) instead of a GL target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:29:28 +03:00
43c379aa0f refactor(render3d): vertex data and draws behind gpu.ludic
Every vertex array, vertex and index buffer, attribute pointer, instance divisor, stream
upload and draw call now goes through gpu_mesh_* / gpu_buffer_* / gpu_draw_*, and no other
file in the package names them. A Mesh records its layout as it is built - which buffer
feeds which attribute at what stride and offset, per vertex or per instance - so a backend
that bakes vertex input into a pipeline can read it back. On OpenGL each call is the GL it
replaces, in the same order: the five fixed viewpoints render bit-identically and the game's
self-tests report exactly what they did before.

scatter_attach takes the mesh rather than its vertex array; a mesh now frees every vertex
buffer it owns (glTF meshes used to keep all but the first); the two helpers nothing called,
mesh_grid_patches and mesh_instance_buffer, are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:21:55 +03:00
54eeef5f8d feat(render3d): gpu.ludic - the seam for a second graphics API, and what the GPU can do
Render state (depth, blending, culling, colour writes, alpha-to-coverage, depth bias,
scissor) and uniforms go through gpu_* / u_* and nowhere else; OpenGL state is cached and
only changes reach the driver. Frames are bit-identical to before at the fixed viewpoints.
R3D_GFX=gl|vk or gpu_request chooses a backend, falling back to OpenGL with a reason.

gpu_caps_probe() asks Vulkan, on Windows, for the 1.3 floor, ray tracing, mesh shaders, the
NVIDIA RTX generation, Reflex and HDR colour spaces, for a game's settings to grey out what
a machine cannot use; R3D_CAPS pretends to be a given card for tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:52:12 +03:00
d6ffdfa638 perf(render3d): a depth prepass for the near tree foliage
Needle-card crowns are many cut-out quads deep and a discarding shader turns early depth
rejection off, so every card behind the front one ran the full lighting shader. The near
tree LODs now write depth first (depth.frag, the same alpha coverage test), terrain under
them is rejected before shading, and the lit pass draws them with no discard and an equal
depth test (invariant gl_Position). R3D_NOPREPASS=1 restores the old path.

Dense forest on a Windows PC: 61 -> 78 fps at 3840x2160, 116 -> 164 fps at 1920x1080.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 04:18:38 +03:00
a4a3d75cd9 feat(render3d): replace the world at run time, water bodies, terrain_sea
terrain_reload/terrain_unload release one map's height field, survey, photograph
and patch bounds and generate another at any TERRAIN_HALF; scatter_clear_all
(with streams), actor_clear_all, col_reset and mesh_free empty the scene;
water_body_add/water_bodies_clear draw several still-water planes with one
reflecting; terrain_sea separates the coast's sea level from the carved lake's,
and grass keeps off both. Fixes CDLOD patch bounds for TERRAIN_HALF != 4096 and
water_init leaking a mesh and program per call. examples/rendering/reload.ludic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:03:40 +03:00
f25289db20 feat(gl): OpenGL 4.1 and the ludic.render3d renderer
Some checks failed
ci / build-and-test (push) Waiting to run
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Has been cancelled
docs / build-and-deploy (push) Successful in 34s
`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the
platform gl3.h with every GL_* constant, generated by `ludic-dev glgen`
with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the
existing window at Retina resolution; headless builds render into an
offscreen CGL context, so a program that uses Gl.* renders and
screenshots identically under the test harness. It links gl.ll, the
thunks and OpenGL.framework only when used; every other build stays
byte-identical.

packages/ludic.render3d is a physically based renderer written on that
surface: HDRI image-based lighting, GPU-generated terrain with scanned
PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced
vegetation with impostors, procedural grass, water, SSAO, and an HDR
pipeline with bloom, auto-exposure and ACES.

It also carries this session's work on it: the terrain at half its cost
(10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer
you played, a resize that emptied the world, and the packaging that lets
a game use the renderer from its own repository — `ludic assets`, the
material manifest shipping with the package, and shader lookup falling
back to the install root. See changes/ for each, with its numbers.

The camping game that drove all of it has moved out to its own
repository, Maroon Lake; examples/rendering/smooth.ludic stays as the
renderer's example here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 03:31:12 +03:00