water.frag takes u_wade - a point and a strength - and puts spreading rings and a
patch of churn into the surface normals there, so a wader marks the water and a
swimmer works the whole of it. It is one uniform in a fragment stage that was
already running, applied after the distance flattening so a disturbance close to
the camera survives it, and it measures no frame cost at all.
Also records the two renderer features that shipped without a changeset.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reflecting body is clipped to its ellipse like every other unless it is an
unbounded sea, so a map whose reflection belongs to its lake (Maroon Lake, once
its sea sits below it) does not draw that lake's level over every hollow in the
survey. The terrain's wet shore and its forest and scree gates read the carved
lake's line inside its outline (u_lake), the rule grass already used. SPIR-V
regenerated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every R3D_* read goes through r3d_env_has / r3d_env (env.ludic): a headless
build honours them as before, a windowed build only when R3D_DEV is set to
anything but "0", so a shipped game never reaches a debug view, feature kill,
file writer or hardware fake. Documented in docs/SHIPPING.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The water reflection keeps the main camera's frustum planes, and ac_visible tested the actor itself
against them: the town's people, far above the lake with their images far below the frame, all drew
again. The image (2L - y) is what is tested now - town, 69 skinned reflection draws -> 38, the frame
byte-identical; PC camp reflection scene CPU 793 -> about 760 us. R3D_PROF splits the reflection's
CPU into setup, terrain, scene and sky.
Tried and not kept, with the numbers (PC, camp, Vulkan GPU timestamps):
- the ground's cost is its scanned material taps: without them the terrain pass is 555 us of 1568,
without the photograph 1029; the sun, the noise fields and the grain are 20-100 us each. Moving the
cheap tier in from 200 m to 60 m changes nothing, so it is the far tier's single taps over the
mountains. Skipping the normal-map taps past 900 m (where the detail normal is fully faded) saved
22 us and moved a few pixels - reverted.
- grass: cutting its radius to 300 m barely moves it, so the cost is the near blades' pixels; moving
their three noise fields to the vertices changed nothing (747 us) - reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reflection pass is a second copy of the terrain, the vegetation and the actors, and it ran on
every frame of a map with water - looking straight down at a meadow included. The mirrored body is cut
into rectangles where the ground lies below it (32 m over the height map in 8 x 8 blocks, the sea
beyond it coarse), and the pass runs when one meets the view frustum and its water is not all dry
where it shows, outside the frame or behind the ground. Three wrong turns on the way, each kept in a
comment: bounding spheres (a 156 m cell reached into the view from behind the camera), sight lines
that never asked whether the point was in the frame, and a walk of every rectangle every frame (0.1 s
per 400 frames on the PC until the blocks). Built once in 6.6 ms.
Mac, the five shot viewpoints: view c (the meadow) 225 reflection draws -> none; a, b, d, e unchanged;
OpenGL frames byte-identical; self-tests 59/59 on OpenGL and Vulkan; MoltenVK validation adds nothing.
PC camp (lake in view): 1882 draws either way, 3.8 s for 400 frames either way, three runs each,
self-tests 59/59, validation 0. R3D_REFL_ALWAYS=1 runs the pass every frame; R3D_REFL_DBG=1 prints the
test once.
R3D_ACTOR_CENSUS=<frame> prints the lit pass's actors grouped by model: town is 17 models and 69 draws,
and only four rigid models repeat (17 actors) - too little for instancing to be worth a shader variant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
A water body other than the reflecting one drew its whole bounding rectangle,
so the corners outside the lake's carved ellipse showed water over dry ground;
those bodies now discard outside the ellipse. r3d_fog_base (default 0) is the
height the height fog is measured from, so maps sharing one datum at different
heights get the same air.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The water shader's `u_screen`, and the reflection target's size, were both taken
from gl_w/gl_h - the drawable. But gl_FragCoord in that shader runs over the
scene target, which is post_w x post_h. They match only at a render scale of 1;
at anything less the refraction and depth reads landed in the wrong corner of
the frame and the lake showed a squashed copy of it instead of its own bed.
Both now come from the scene target. The reflection is sized from it too, which
also stops it paying for pixels the water never samples. R3D_DUMP_REFL reads the
target's own w/h rather than recomputing them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`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>