chore(release): v0.7.0
This commit is contained in:
parent
f25289db20
commit
9d1df6ec32
8 changed files with 189 additions and 191 deletions
|
|
@ -1,28 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**OpenGL for Ludic, and a 3D renderer on it.** `Gl.*` binds the whole OpenGL 4.1
|
||||
core API — every `gl*` entry point of the platform `gl3.h` as `Gl.<snake_name>(…)`
|
||||
with every `GL_*` constant, generated by `ludic-dev glgen` with per-call ABI thunks
|
||||
(`runtime/native/gl_thunks.ll`; float/double parameters take `fixed`). Windowed
|
||||
builds get an `NSOpenGLContext` on the existing window at Retina resolution
|
||||
(`cocoa.ll`); headless builds render into an offscreen CGL context, so a program
|
||||
that uses `Gl.*` renders and screenshots identically under the test harness.
|
||||
`Gl.open / swap / screenshot / program / vao / floats …` cover the glue, and the
|
||||
IEEE-float helpers (`f_add`, `mem_put_f32`, …) let Q16.16 programs fill real
|
||||
float vertex and uniform data. `Gl.*` links `gl.ll + gl_thunks.ll + OpenGL.framework`
|
||||
only when used; every other build is byte-identical.
|
||||
|
||||
The `ludic.render3d` package (`packages/ludic.render3d`) is a physically based
|
||||
renderer written on `Gl.*`: HDRI sky with image-based lighting (irradiance, GGX
|
||||
prefiltered, split-sum BRDF, sun extracted from the map), GPU-generated terrain
|
||||
with scanned PBR materials (stochastic anti-tiling, triplanar rock, slope/altitude
|
||||
splatting), cascaded shadow maps with PCF and world-unit biasing, a glTF loader
|
||||
for scanned models, instanced vegetation with baked impostors, procedural grass
|
||||
and lupines with wind and translucency, 4x MSAA with alpha-to-coverage, SSAO,
|
||||
still water, cloud shadows, aerial perspective, an HDR pipeline with bloom,
|
||||
auto-exposure, ACES tonemapping, grading, sharpening and grain. See
|
||||
`examples/rendering/smooth.ludic`, and the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake) for a
|
||||
game built on it. The renderer's CC0 materials are fetched with `ludic assets`.
|
||||
|
||||
**16-bit PNGs**: the renderer's texture loader keeps 16-bit samples (normal /
|
||||
displacement maps) and uploads them as `RGB16` / `R16`.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
An upgrade keeps the package store. The installer replaces the whole install
|
||||
root, and `ludic add` caches packages in `~/.ludic/store` — so re-running the
|
||||
one-liner deleted every package a project had fetched. The store is carried
|
||||
across now; everything else in the root belongs to the toolchain and is replaced.
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A game can use the renderer from outside this repository.** `ludic.render3d` reads two
|
||||
things from disk at run time — its GLSL, and the scanned CC0 materials — and both were
|
||||
found only by a path relative to the working directory, so the renderer worked in a
|
||||
Ludic checkout and nowhere else. A game living in its own repository now needs to copy
|
||||
neither.
|
||||
|
||||
**The shaders come from the package**, wherever the package is. They belong to
|
||||
`ludic.render3d` and ship with it, so the renderer looks for them beside the project
|
||||
first (a Ludic checkout, where they are under `packages/`) and then under the install
|
||||
root, `$LUDIC_HOME/packages/ludic.render3d` — the same place the compiler already
|
||||
resolves `import "ludic.render3d/r3d.ludic"` from. Nothing to vendor, and no version of
|
||||
the shaders that can drift from the version of the code that compiles them.
|
||||
|
||||
**`ludic assets [--force]`** fetches the scanned materials and the HDRI sky into
|
||||
`assets/polyhaven/` of whatever project you run it in. The list of what to fetch is the
|
||||
renderer's own — the renderer decides which materials it wants — so it moved out of the
|
||||
repository's `assets/` and into the package as
|
||||
`packages/ludic.render3d/assets.manifest`, where it ships with the toolchain. A game
|
||||
does not keep its own copy of that list and so cannot fall out of step with the
|
||||
renderer's material set. `ludic dev fetch-assets` is the same command from a checkout.
|
||||
|
||||
**A URL-shaped module built its binary into directories.** `project_name` took
|
||||
everything after the last dot of the manifest's module path, which for a package
|
||||
identified the way the package manager identifies them —
|
||||
`git.host.io/user/name` — is inside the *host*: the build wrote
|
||||
`build/io/user/name` instead of `build/name`. The last path segment comes first now, and
|
||||
a dot inside that segment still separates namespace from package, so `ludic.render3d`
|
||||
still builds as `render3d`.
|
||||
|
||||
**The camping game has moved out** to its own repository —
|
||||
[Maroon Lake](https://git.workshopsoft.io/workshopsoft/maroon-lake) — taking
|
||||
`examples/rendering/valley.ludic`, `hiker.ludic`, `camp/`, and 175 MB of survey data and
|
||||
scanned kit with it. It was here as a demo of the renderer and became a game, and an
|
||||
engine repository should not be carrying a game's assets. It is now the first consumer
|
||||
of everything above, which is the point: what the renderer needs a game to be able to do,
|
||||
it can now do from outside. `examples/rendering/smooth.ludic` stays as the renderer's
|
||||
example in this tree.
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**Resizing the window (or entering fullscreen) no longer empties the world.** It left
|
||||
`gl error 1286` — `GL_INVALID_FRAMEBUFFER_OPERATION` — on every frame from there on, with
|
||||
the terrain, the trees, the grass and the water gone and only the sky drawn.
|
||||
|
||||
The sun-visibility pass added in `changes/terrain-perf.md` borrows the depth buffer the
|
||||
frame is about to be drawn with, so that rasterising it doubles as a depth prepass. It
|
||||
was storing that borrowed texture in its `Target`, and a `Target` deletes whatever its
|
||||
`depth` names when it is freed. On the first resize the sequence was: `post_free` deletes
|
||||
the frame's depth texture, `post_init` immediately makes the replacement — and GL hands
|
||||
back the name that was just freed — and then the visibility target, rebuilt for the new
|
||||
size, deleted that name believing it was its own. The scene framebuffer lost its depth
|
||||
attachment. What is left is a colour-only framebuffer, which is *complete*, so drawing
|
||||
carried on with no depth test at all: the sky is a fullscreen quad drawn last, and with
|
||||
nothing left to fail against it painted over the entire valley. The 1286s came from the
|
||||
passes whose own attachment now named a texture that no longer existed.
|
||||
|
||||
A borrowed attachment is never written into the target now, and the frame's depth is
|
||||
attached afresh at the start of each pass — it is a different texture every time the
|
||||
screen-sized buffers are rebuilt, and one `glFramebufferTexture2D` per pass is cheaper
|
||||
than any scheme for noticing that it changed.
|
||||
|
||||
`R3D_RESIZE_AT=<frame>` rebuilds every screen-sized buffer from that frame on, cycling
|
||||
through four drawable sizes every few frames. A window cannot be resized in a headless
|
||||
run, so this is the only way to reach the path; it reproduced the fault in one frame and
|
||||
now runs twenty resizes, with the fly camera and with the game, without an error.
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
bump: patch
|
||||
type: perf
|
||||
**Ground cover stops re-growing itself.** Walking a streamed world hitched, and the
|
||||
hitch got worse the longer you played. Measured in the Maroon Lake game, with a new hitch
|
||||
report rather than guessed at.
|
||||
|
||||
**The chunk cache had a cliff, not a slope.** A stream cached 4096 chunks and then
|
||||
stopped remembering: past that the chunk was generated, used for one frame and thrown
|
||||
away, so every ring walk regenerated it, for the rest of the session. It arrives after
|
||||
enough of the map has been walked — six evictions' worth over seven kilometres, so an
|
||||
ordinary session reaches it — and it is the point where cover starts visibly re-growing
|
||||
as you turn. `stream_evict` now drops the half of the cache nobody has asked for in the
|
||||
longest time (chunks carry the walk that last wanted them) and rebuilds the index over
|
||||
what is left. Over a 7 km traversal: generation total **9073 ms → 2230 ms**, the worst
|
||||
single frame's generation **11.4 ms → 3.1 ms**, median frame 10.8 → 9.0 ms. With a cache
|
||||
deliberately sized to saturate early, the same run goes from 2748 frames generating to
|
||||
1548, and from a 13.3 ms median to 9.2. `R3D_NOEVICT` restores the old behaviour for
|
||||
comparison, `R3D_STREAM_CAP=<n>` sets the cache size.
|
||||
|
||||
The other half of that hitch was in the game's own cover generator, and went with it to
|
||||
the Maroon Lake game's repository: its candidates were paying for a second noise field, four
|
||||
height samples and a path distance before the drift field that rules out most of the
|
||||
meadow — 2341 µs → 518 µs per chunk, bit-identical output. Worth repeating in any
|
||||
generator: a `stream_fill` is called for tens of thousands of candidates per chunk, so
|
||||
the order of its tests is most of its cost.
|
||||
|
||||
**The hitch report** (`R3D_PROF=1`) is what found both. It prints the slowest frames of
|
||||
the run with what was in each: CPU versus GPU wait, cover generated, instance bytes
|
||||
uploaded, the game's own tick, and the renderer phase that took longest. Alongside it,
|
||||
per-chunk generation cost by stream and band, a census of what the caches hold, and a
|
||||
stutter figure — the frame time a run spent beyond 1.2x its own median — because a mean
|
||||
cannot show a hitch and a maximum is one unlucky frame.
|
||||
|
||||
It also found two content bugs in the game it was measured on, which is the report doing
|
||||
its job: a cover stream whose placement rule never fires still pays full generation cost,
|
||||
and the census makes that visible — 3364 cached chunks holding zero instances.
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
bump: patch
|
||||
type: perf
|
||||
**The ground costs half what it did.** Measured in the Maroon Lake game, the terrain was 10.3 ms of
|
||||
a 22.2 ms frame; it is now 5.4 ms of 16.4 ms — 45 fps to 61 fps at 1080p, with the
|
||||
frame otherwise unchanged (every viewpoint tested stays above 54 dB PSNR against the
|
||||
old renderer, with no channel differing by more than 7/255).
|
||||
|
||||
**Measure by frame time, not by the pass timers.** `R3D_PROF`'s per-pass
|
||||
`GL_TIME_ELAPSED` queries cannot be trusted on this driver: with the ground's shading
|
||||
work removed the terrain query fell from 10.5 ms to 1.3 ms while the frame time did
|
||||
not move at all. Every number above and below is a median real frame time, taken by
|
||||
switching one thing off (`prof_ft_report`); the pass timers are still printed, and are
|
||||
still useful for spotting a pass that appears out of nowhere, but they cannot size one.
|
||||
`R3D_NOTERRAIN` skips the ground, `R3D_TNEARONLY` / `R3D_TFARONLY` draw every patch
|
||||
with one tier's program, and `R3D_RES=<w>x<h>` renders at another size — the three
|
||||
switches that say whether a cost is the ground, which tier it is in, and whether it is
|
||||
pixels at all.
|
||||
|
||||
**Each detail tier is its own program.** `terrain.frag` holds a detailed near tier and
|
||||
a cheap far one and chose between them per pixel, so every pixel of the valley walls
|
||||
was compiled — and scheduled — for a near path it never ran. CDLOD selection now knows
|
||||
which tiers a patch can contain: one that never comes within the split draws with
|
||||
`FAR_ONLY`, one wholly inside it with `NEAR_ONLY`, and only the ring of patches that
|
||||
straddle the band needs the program that holds both and cross-fades. Pixel-identical,
|
||||
and it makes the tiers separately measurable: the near tier costs 7.5 ms over a whole
|
||||
frame, the far tier 2.3 ms.
|
||||
|
||||
**The sun visibility is its own pass** (`tersun.frag`). The same CDLOD patches are
|
||||
rasterised once into a screen-sized R8 buffer that holds nothing but each ground
|
||||
pixel's sun visibility, and `terrain.frag` fetches it by fragment coordinate. The pass
|
||||
costs 0.27 ms, shares the frame's depth buffer so it doubles as a depth prepass, and
|
||||
takes the cascade read out of the shader that covers the screen. It picks its tier —
|
||||
filtered PCF near, a single tap far — over the same cross-faded band the ground uses,
|
||||
so the boundary is not a contour you can find on the hillside.
|
||||
|
||||
**Nothing is sampled for a weight of zero.** The ground sampled all four of its
|
||||
materials for every pixel and then blended three of them at zero: a meadow pixel took
|
||||
nine taps of triplanar rock, a cliff pixel nine taps of stochastic grass, and every
|
||||
pixel in the valley took the snow tile and the four noise fields behind the lake's
|
||||
shore wash — a wash that is a hairline along one shore within 120 m of the camera. The
|
||||
survey photograph's classification now runs first, because it is what decides which
|
||||
materials are present; each material block sits behind its own weight; the ridge field
|
||||
that ragged the snow line is skipped 160 m below it, where it cannot change anything;
|
||||
and inside the stochastic blend a cell's rotation, offset and rotated gradients are
|
||||
computed inside its own test, so a cell whose sharpened weight rounds away costs
|
||||
nothing. All of it exact where the weight is zero, and it is most of the win.
|
||||
|
||||
Material sampling is what remains (2.8 ms of the 5.4): scanned 2K tiles taken at 16x
|
||||
anisotropy on ground seen at a grazing angle. `R3D_ANISO=<n>` sets the filter (the
|
||||
default is unchanged at 16; 4 is worth 1.0 ms and 1 is worth 1.7 ms).
|
||||
|
||||
The shadow pass is 1.2 ms of the frame and has nothing to give: re-using the far
|
||||
cascades between frames — their windows are snapped to a 14 m and a 64 m grid — is
|
||||
worth 0.15 ms standing still and nothing while walking, so it is not in the tree.
|
||||
Loading…
Add table
Add a link
Reference in a new issue