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>
- sky_start_yaw: a game that turns the sky at boot sets it before r3d_init and the image-based light
is baked once at that yaw; sky_set_yaw to a yaw already baked bakes nothing.
- terrain_init_step: the height field and normals, the sun shadow, the materials, the patches and
programs as separate steps; r3d_load_count is 8, so a loading bar moves through the terrain (half
the start-up) instead of jumping over it. terrain_init runs the four in a row as before.
- The per-cascade actor cull centres its sphere at half the model's height and sizes it by the
larger of height and radius: centred at the radius, it dropped a head at a cascade's edge (11 px
in town, found by the drawstats comparison). Lake was byte-identical before and after.
OpenGL frames at the five viewpoints unchanged; the game's 59 self-tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- r3d_open opens the window with nothing baked; r3d_load_count / r3d_load_step run the rest (sky
and light, terrain, shadow and screen targets, cover and actors, grass) so a game can present a
loading frame between them. r3d_init is the same calls in a row.
- sky_set_quality(width): the prefiltered sky light at 256 / 512 / 1024, baked again.
- post_set_msaa(samples): multisampling on OpenGL, remade in place (post_msaa_live is false on
Vulkan); post_free frees the multisampled framebuffer too.
- STREAM_BUDGET_US is a variable; r3d_fog_scale multiplies the fog the day sets.
- Vulkan buffers carry UNIFORM_BUFFER usage: the frame's ring is bound as uniform buffers and was
created without it (VUID-VkWriteDescriptorSet-descriptorType-00330, found on MoltenVK).
OpenGL frames at the five viewpoints unchanged; the game's 59 self-tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A game ended its frame with Gl.swap() and shot it with Gl.screenshot, which tied it to OpenGL
and made it name Gl.* only so the parser would splice the GL runtime. gpu_present and
gpu_screenshot carry both (and name Gl.* themselves, so importing render3d is enough), and
r3d_present / r3d_screenshot are what a game calls. The Vulkan backend takes both over.
OpenGL frames byte-identical at the five viewpoints; 59 self-tests pass with no GL error.
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>
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>
The queue was only emptied by the next outline_model call, so the last highlighted tree kept
its rim after the player looked away. r3d_frame now calls outline_frame, which drops a batch
the previous frame closed and nobody reopened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
`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>