A step is (kind, param, need) and a question about state: a kind has a
shape (yes, a count, a mask counted, every bit of a mask) and a pool
(world, any, sum, or, max, each), handed in by the game as StepsKind
records. An arc is chapters of up to STEPS_PER_CHAPTER steps; steps_check
sticks each met step of the current chapter (STEPS_MET) and opens the
next when all are (STEPS_CHAPTER), so a step met before its chapter
ticks at once. With company each seat's shares are held per chapter,
pooled as the kind says (steps_met, steps_party_got), counted for an
EACH step (steps_each, steps_lacking), and a leaver takes theirs along.
Port: StepsWorld { value(kind, param, player) (required), party,
present }. The package knows no kind's meaning and no chapter's words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
latest.json asked once and polled (the newest version and its notes, in
a language or English); then Velopack's releases.<os>.json, the plan (the
delta chain when the installed full package is on disk, no delta is
missing and the chain weighs less than the full one; the newest full
package otherwise, named as the feed names it), each package streamed to
the packages folder with its bytes on a bar and an eased pace, its
SHA-256 by Get-FileHash or shasum in a child, Update patch per delta and
Update apply --waitPid. macOS keeps its packages outside the bundle and
names --rootDir and --packageDir; a translocated app is not installed.
Ports with the runtime as every default: UpdateWorld (platform, exe, pid,
now_ms), UpdateNet (get, download, poll, text, received, free),
UpdateProc (spawn, poll, free). Config: feed, app_id, version, root,
packages, mac_packages, scratch. It words nothing: states, UPDATE_ERR_*
codes and numbers, and UPDATE_STARTED / _STOPPED / _APPLYING facts - the
game exits on the last.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ludic.clock, .effects, .inventory, .wallet, .weather and .tracks say
'uses ludic_base' (ludic.base uses nothing). ClockWorld, PackRules,
WeatherWorld and TracksWorld are export ports whose defaults are the old
fallbacks; clock_bind, inv_bind, weather_bind and tracks_bind are gone,
and the tests bind their fakes. The base README's toy does the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Events are encoded once into lines held in memory; a batch is the first
lines joined, with the player id put in where a mark stood, POSTed on
Http's own thread every flush_ms or at flush_at events; a failure keeps
its lines and doubles the wait up to backoff_max; the queue is on disk
every save_ms and read back at the next start; off (the enabled port)
drops the request in flight, empties the queue and deletes the file; and
a run that may not send (can_send) keeps nothing. The player id is the
machine's own id (MachineGuid, IOPlatformUUID, /etc/machine-id) hashed
with the game's salt, else random; it lives in the game's id file beside
whatever else the game keeps there.
Ports: TelemetryWorld (can_send, enabled, now_ms, stamp) and
TelemetryTransport (send, poll, drop; unbound: Http). Config is a record
(host, key, path, lib, queue_file, id_file, id_salt, and the timings).
Facts: TELEMETRY_SENT, _FAILED, _ID. Not a System: it runs from a
launcher's first frame, before any world exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner's list starts from the declared systems, in the order the
compiler gives an open registry, and core_add appends after them.
registry_test covers it; the README says ludic test runs the package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ring of prints per kind; a port for the trip's time, the reader's tier, a
bearing in words and a hook the game draws each print through. A read is a
TrackRead fact on tracks_reads() every time and a count the first time; other
sign is told with tracks_note. The count and last bearing are its save section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kinds and their odds are data the game hands in; a port asks whether this
machine owns the world, the hour, the night, the odds from a kind, what the
story wants of the sky (a front, a hold, a clear night), the front's kind and
length, and a seed. Its own dice; a new spell, a clearing, lightning and a
wanted front are facts on weather_facts(); its own save section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ludic.inventory: counts per item kind, a PackRules port (stack_limit), add
what fits / take all or none / set outright, an ItemChanged queue, and a save
section keyed by item NAME so a game can reorder its items with no migration.
ludic.wallet: earn, spend only what is there, lose down to zero, set, a
MoneyChanged queue and its own save section. Six and five test blocks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ClockWorld port (owns_world, rest_scale, seed), verbs to set and advance it,
clock_new_day, a DayTurned queue (stayed up past midnight, or slept), a
calendar whose month, year and season are pure functions of the day, and a
System (PH_INPUT) with its own save section. Nine test blocks in
tests/clock_test.ludic with fakes for the port.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ludic.effects: effects_add / effects_add_after / effects_clear / effects_run,
effects_sum / effects_has / effects_at / effects_live, an EffectEnded queue
(ran out, pushed out by the bonus cap, crowded out of a full ring) and a
System with its own save section. Nine test blocks in tests/effects_test.ludic.
ludic.base: q_new collided with render3d's quaternion q_new the moment a game
imported both, so the queue constructor is queue_new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mechanic package depends on ludic.base and nothing else: ports (records of
function values for now) for questions, queues for facts, verbs for changes,
phases for order and its own versioned save section. The runner inits, resets,
saves and loads systems in the order added and ticks them phase by phase; a
missing save section is a reset. Tests for each piece and a worked route
between two toy mechanics live under tests/. The old ludic.core (engine ECS
components) is used by examples/library and stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the scrollbar is .ui-scrollbar with a .ui-thumb, styled by the default sheet or a theme; a press
on the thumb holds it at the point it was taken and the pointer moves it in proportion, a press on
the track jumps the thumb's middle there and holds it, and neither presses what is under the bar
(it used to centre the thumb on the pointer and press the row beneath as well);
- while a popover is up only scroll boxes inside it take the pointer, and a press outside it that
closes it forgets any press in progress;
- a transition ends exactly on its value (it stopped a rounding error short, at every frame rate).
Tests: ui_popover_mouse, ui_scrollbar, ui_ease (60/120/180/240 Hz).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- <input type="number" min max step>: typed digits replace its value (a minus when min allows one,
a point when step has a fraction), Enter or leaving the field commits them held between min and
max, the left and right arrows step it.
- <input type="key">: Enter or a click starts it listening and the next key is its value, Tab, the
arrows and Enter included; Esc stops it listening, Backspace clears it, `shown` names the value,
and ui_capturing() tells the host to leave its keys alone (through the frame that ended it).
- note="..." under any control's label (.ui-labels > .ui-label + .ui-note); a range's (or number's)
value with decimals, format="percent" and a unit.
- A popover with `anchor="id"` (the nearest element of that id) or a bare `anchor` (the element
before it) sits beside it by `placement` (right, left, bottom, top), its margin the gap, opens to
the other side where it would leave the screen, and is kept on it.
- A tooltip's title splits into lines at a newline or a written \n.
- text-shadow, inherited, drawn sharp under the text.
- ui_opacity(): the group opacity a native's draw is at.
- border-image is drawn as painted with no background colour, tinted by one, and not at all when
that colour is transparent (it drew a white nine-slice).
- A picture file is drawn untinted, as a browser draws one; an atlas cell (prefix:name) still takes
the color around it, and an <img> with its own color is tinted by it.
- The render3d backend loads a picture again when its file changes or appears (a failed load was
kept for ever), and at once after ui_image_reload(path); takes a path with a drive letter (C:/...)
as a path rather than an atlas; and slices a nine-slice by its texture's own width and height.
- ui_inputs.ludic and ui_look.ludic; LANGUAGE.md and the changeset say all of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- flex-shrink: a line whose children want more room than it has takes it back from those that
shrink, in proportion to shrink times size and never below a min size, a fixed size or the
content. A scroll box shrinks (and scrolls), and a box in a column shrinks as far as the scroll
boxes inside it let it, so a list in a column takes the room its siblings leave with no height.
`flex: grow shrink`, and `flex-shrink` by name.
- A row wider than its room measures its texts (and boxes without a width) again in the room the
rest leave them, so a line wraps beside an icon.
- calc() with + - * / and brackets over px, %, em, rem, vw, vh, plain numbers and var(); a width
or height keeps its percentage, taken of the room it is given. Lengths lists (padding, margin)
keep a calc()'s spaces.
- `order` places a box's children without touching the tree.
- width: 0 and height: 0 are 0: an unset size is UI_AUTO now, not 0.
- A component root that is itself a component takes each user's class, style and id in turn (the
outermost's id wins), and the rules of all their sheets are weighed by specificity together; a
user's rule wins a tie. The parent's sheet used to override the child's whatever its specificity,
and a middle component's sheet was lost.
- The default sheet lays .ui-track out as a row, so a range's fill is as tall as its track and a
checked box's knob goes right; a range's thumb is centred on the fill's end.
- ui_boxes.ludic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An action whose first word is `set` or `emit` followed by `(` is a call, so a component's
`on set(v: int)` is pressed as `set(4)`; `set x = ...` is still the action.
- A `string` prop (or state, or parameter) given a number in a template reads it as text through
ludic.ui's new `ui_val_text`; `label="{3}"` used to arrive as "".
- Two components of one name (or of names that differ only in case, which share their functions'
names) are an error at the second declaration that names the first's file and line, instead of a
"function cmp_x_def_y is defined twice".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `popover`: a top layer that keeps the pointer and the keyboard, closed by a press outside or Esc.
- `title` tooltips after half a second's rest, styled by .ui-tooltip.
- `<progress>` and `<meter>`.
- Atlases take rows and number cells, and importing ludic.ui/render3d.ludic is enough to draw with
render3d.
- The compiler reports two declarations of one name before it type-checks, so a package global
clashing with a program's reads as that, not as 29 type errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game no longer writes a host:
- The runtime reads Input itself: focus and keyboard navigation (Tab, arrows, Enter/Space,
autofocus), the pointer (hover, :active, click on release, drag), and scroll boxes with the
wheel, a draggable scrollbar, clipping and scroll-into-view.
- HTML's controls are built in (button, checkbox, radio, range, select, text, key capture), made
of plain parts a stylesheet styles, each reporting with on-change and event.value.
- ludic.ui/render3d.ludic draws with render3d's overlay: textures, named atlases (icon:NAME),
nine-slice border-image, rounded rects and rings, clipping, and a scale.
- Hooks for the program's language, sounds and clock (ui_translator, ui_sounds, ui_clock).
- ui_dev: hot reload, errors on screen, LUDIC_UI_DUMP.
- CSS:
- colours as #rgb / #rrggbbaa / rgb() / rgba() / names;
- border-radius and outline (following the radius), box-shadow, background-image and a tinted
border-image;
- group opacity, @keyframes / animation, transition;
- :focus, :focus-visible and :focus-within.
- HTML mixed content, and boolean attributes.
- render3d gains tex_width / tex_height, and the XML reader keeps text runs in order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The compiler turns a component declaration into:
- a record of its props and state;
- a constructor, a props setter, a model and a call;
- a class, registered with ludic.ui at start.
Its template and styles are read from beside it and compiled in, with @import inlined, and a
missing template fails the build.
In the runtime:
- each mounted instance keeps its own props and state, and `set` in a template writes the state;
- styles are scoped to the component;
- class, style and id on a component's tag land on its root, styled by the parent's sheet too;
- ui_reload re-reads a component's files from disk and keeps instances.
The package's own module is now ludic_ui, so a program may call a directory of its own ui.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Natives: ui_native(tag, measure, draw) makes an element the program draws itself; ui_fire
runs its on-<event> with event.value. input, select and textarea have simple defaults.
- React:
- keyed <each>, <let>, <provide> context through components, <fragment>;
- named slots, default props on <component>;
- on-mount / on-unmount;
- inline text inside text elements.
- CSS:
- custom properties and var();
- position relative/absolute/fixed with insets and z-index;
- em/rem/vw/vh and @media;
- wrapping text and ellipsis, overflow;
- + and ~ combinators, :nth-child(an+b), :checked, :active.
- Developing: errors with file:line, ui_errors(), ui_reload() keeping state, and an
inspector-style ui_dump.
- view fields infer the type of a literal or a named function's result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `view Name { field = x; function q(..); on e(..) }` declaration is the one bridge between a
program and its UI: it writes view_<name>() -> UiView, whose model is a Value of every field and
whose call runs a query or an event by name.
ludic.ui is a template runtime:
- HTML-shaped XML screens and components, loaded at run time;
- {expression} bindings, if/else/each, props, slots, per-instance state;
- on-press / onclick actions (event, set, emit);
- component libraries (export="true", <import src as>).
Styling:
- stylesheets in <style> or importable .lss files (@import);
- CSS selectors (#id, .class, [attr=v], descendant and > combinators, :hover, :disabled,
:first-child, :last-child, :nth-child, :not) weighed by specificity;
- the box model and flex under CSS's property names.
Also:
- default parameters, and positional-then-named calls;
- Value gains a float kind;
- a function shadowing a runtime one is refused;
- an index is evaluated before the slice is read;
- runtime errors name the right file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.
What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
selfhost/check/ walks every function, the entry, tests, globals' initializers and
@On listeners with real scopes, and refuses mixed number kinds, text and numbers,
two record types, mismatched slices and fn types, wrong argument counts, wrong
returns and wrong push elements - every mix-up at once, each at its line.
LUDIC_CHECK_REPORT=1 lists them by category. pointer stays untyped (L7's).
What it found is fixed: render3d's HDR scan calling the float-bits extern f_lt
with floats; ludic.shooter's right-stick aim overflowing past half a push;
prof.ludic storing longs in []int; extern arguments now coerced to their
parameters. Text-returning runtime functions say string; Assets.ready says bool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fn(int, float) -> bool is a type, fn name is any top-level function's value, and a call through
a local, a global, a record field, a slice element, a parameter or a result of a function type
is an indirect call; two function types mix only when equal, a call checks its argument count,
and a value may be null (examples/functions/values.ludic). Job.parallel_for keeps its worker
check.
render3d's scene is registered rather than required by name: r3d_on_draw, r3d_on_casters and
r3d_on_stream_fill (hooks.ludic). The two rendering examples register theirs - and had defined
scene_draw_casters with no parameter while the renderer passed one, which nothing checked.
render3d declares numbers float itself; smooth.ludic is converted to floats and returns when
r3d_init fails instead of running on into a segfault. Noise.* check their argument count (a call
one short crashed the compiler). selfhost-build says why it failed. The migration tool reads a
declared float as evidence. Seed regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A numbers float file adapts decimal literals to a fixed operand or slot, and refuses to
promote a computed int to a float implicitly: there it is almost always float bits. Explicit
float(x) is always allowed.
render3d's numbers are float, converted by tools/migrate/floatbits.py - a whole-program
inference of which ints carried IEEE bits (union-find over flows, calls, returns, buffers,
nested buffers and lexical scopes) and a rewriter to operators, Math.* and float literals,
with float_bits / float_from_bits left only where bits really cross (runtime scratch
buffers, mixed buffers). Seed regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Input.text, App.monitor_count / window_to_monitor / window_fixed,
gl_sleep_us, and the grass and collide changes, uncommitted on main and
depended on by the game; carried here so the language work starts from
what the game actually uses. main's working tree is untouched.
A slice has carried { ptr, len, cap } since it existed and nothing ever read
the len: every index emitted a bare getelementptr. Running off the end of one
wrote into whatever the allocator had put next, and the program died somewhere
else entirely - a maroon-lake crash took a day to find because the stack named
a texture upload and the write was in a telemetry buffer five frames earlier.
Now each index loads the length and compares unsigned, which rejects a negative
index in the same instruction, and a failure aborts with the location the other
located errors use:
oob.ludic:9: index out of range: 7, len 3
`words` and the other raw buffers are unchanged - they are a bare malloc with no
length to check, which is the argument for moving off them.
The SPIR-V variants are regenerated in the same commit: shaders --check was
failing against the edited GLSL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
layer_flutter's mask was the vertex's height alone, so on a ten metre aspen every part of
the trunk above 1.2 m trembled with the leaves and the bole read as cloth. What separates
a leaf from a bole is distance from the model's centre line, not height; the mask is
radial now, and small plants keep the old one because a flower is all leaf.
NEAR_FADE dithers tree foliage out inside arm's length of the camera, in the depth
prepass, so the lit pass never sees those pixels and the equal-depth optimisation is
untouched. gl_Position.w is the view depth, so it costs one varying and no uniform.
Blades, cards and flowers are excluded - they live at the player's feet - and the shadow
pass keeps every leaf.
daylight_hand takes a reach in metres. The falloff was an inverse square windowed between
26 and 6 with both numbers hard-coded: two per cent of its own near field at ten metres,
so a torch lit your boots. It is a gentle power out to the reach now.
Measured in Maroon Lake, twice per side: the crown mask moves 1.7% of an aspen frame and
nothing at all in the meadow under it; the near fade moves 7.1% of a first-person frame
at a conifer and 0% where there is no foliage in the lens; the torch's reach lifts the lit
ground 20 to 45% and leaves the sky bit-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Actor.cast_hidden separates drawn from casting. ac_visible rejected a hidden actor from
the shadow pass too, so hiding the player's own body for first person took the player's
shadow with it - and in a sunlit basin your own shadow is what tells you where you are.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
layer_flutter(l, v) gives a scatter layer a per-leaf tremble. The vertex stage offsets
each leaf by a phase taken from its own place on the card, so neighbouring leaves are
never in step, and writes the result out as a varying the fragment stage uses to flash
the leaf's pale underside as it turns - which is the part that reads, since a still
frame of a tremble is a still frame of nothing. One uniform, one varying, no extra pass.
And the sway itself was measured in METRES: hgt * hgt * 0.35 is right for a 40 cm
flower and puts ten metres of sideways into a 14 m trunk, so every tall tree in the
valley stood bent over like a fishing rod. It is a fraction of the model's own height
now - the tip moves a few per cent of the tree whatever the tree is, and the base does
not move at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The existing occlusion is tuned as AMBIENT occlusion: a wide radius answering "how open is
the sky here". That is the right question, and it leaves every object in the frame
hovering - because what says a log is ON the ground rather than in front of it is a hard,
narrow darkening in the last few centimetres where the two meet, and at a metre and a half
of radius that darkening is spread so thin it is not there.
A second, tight pass: eight taps inside 40 cm, which at any normal distance is a handful of
pixels and stays in cache, with a much tighter range check than the ambient one so a wall
across the room does not darken the floor in front of it.
Small by frame area, because contact occlusion is - 1.8% of the frame, 7% of the region
around the things it grounds. GL 7.0 -> 7.1 s, VK 7.3 s over 400 frames.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rain fell and the ground did not change: the weather was a curtain of particles in front
of a dry valley.
u_wet, 0 dry to 1 soaked, does the three things a wet surface does. Darkens the albedo.
Drops the roughness hard - which is what makes a soaked meadow read as soaked rather than
merely dark, because the sky glances off it. And pools: water finds the low places and
flat ground holds what a slope sheds, so the puddle mask is the macro noise the materials
already use gated on slope, and a puddle takes the world's up for its normal rather than
the ground's relief.
It joins the shore's existing wet band rather than fighting it - the same term from a
different cause.
34.1% of the frame changes between dry and soaked. GL 7.1 s either way over 400 frames.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
You walked through a meadow and every blade ignored you, which is the most noticeable
thing missing from every step the game asks you to take.
u_push (x, z, radius) bends blades away from a body and DOWN - a trodden stem is shorter
as well as leaning, and leaving the height alone made them splay outward like a fan
instead of being walked through. Applied after the blade's own yaw has put it into world
axes and before it is tipped onto the ground normal, so the push is a world direction
rather than something in the blade's private frame.
Radius 0 means nobody is there, so nothing is paid for when it does not apply. Measured:
GL 6.9 s, VK 7.3 s over 400 frames, unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The grass had a gust built from two sines; the trees had no gust term AT ALL, only a
per-instance wobble. So the meadow rippled and the canopy above it swayed to an unrelated
rhythm, and nothing ever crossed the valley.
wind.glsl is prepended to every stage (programs.ludic, and shaders.ludic for the SPIR-V
build) so grass, crowns and water read the same field at the same world position. It
declares no uniforms on purpose: u_time and u_wind already exist in several of those
files and redeclaring them is a compile error in whichever stage includes both.
The wavelength is what made it work. At 0.030 the front was 209 m crest to crest -
longer than the meadow you can see - so the whole frame sat in one phase and the gust
read as everything breathing together. At 0.085 it is 74 m and a front crosses a view in
a couple of seconds.
R3D_DEBUG_WIND=1 paints the field on the ground. It is the only honest way to show a gust
in a still image, and two shots 0.3 s apart move by 39/255.
400 frames: GL 6.9 s, VK 7.2 s. Backends agree to 0.68/255.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes that have been sitting uncommitted in this checkout, gathered as a recovery
point before the next run of work.
grass.vert: the blade existence gate tested green DOMINANCE - g - max(r, b) - which is a
test for lush green and nothing else. A dry alpine meadow is yellow-green, its red as high
as its green, so the meadow scored zero and was thinned to the floor - a QUARTER of the
blades - on exactly the ground that should be thickest. Measured on Maroon's own ortho,
g - max(r, b) reads +0.026 at the camp and -0.002 six hundred metres away, flipping between
full density and a quarter over continuous meadow; g - b reads +0.076 and +0.014 and
separates plant from rock and snow just as well, because rock and snow are neutral and
vegetation is not. Bare ground in the near band went 82% -> 57% looking down.
Blade height is biased short (h3 * h3) rather than spread evenly, so a meadow is a dense
mat with taller stems out of it; an even spread read as a lawn that had been cut.
water.frag: the planar reflection is returned at 0.72 of the scene's exposure rather than
all of it, the fresnel mix caps at 0.70 rather than 0.86, absorption falls so the bed is
visible through the surface at the angles a player stands at, and the ripple field is
roughly halved - so a sheltered lake is one sheet of water with a mountain in it instead
of open chop to the horizon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shipping default had NO anti-aliasing at all: the temporal resolve was removed, the
setting's first option went on saying "Temporal", and MSAA defaults to one sample, so
every machine without DLSS drew grass and needle cards with nothing smoothing an edge.
FXAA in the sharpen pass, which already reads the neighbourhood and runs last on the LDR
image. No history, so it cannot drag or smear a reflection. 25.5% less single-pixel
staircase on edge pixels.
The trap, recorded because it inverted the result: the unsharp delta must be computed
from the RAW image and only then applied to the anti-aliased colour. Centre from FXAA and
neighbours from the raw texture measures half smoothing and half signal, so the mask
sharpens precisely what FXAA softened - 29% WORSE than no anti-aliasing at all.
Depth of field for the photo mode: a disc whose radius is the circle of confusion,
normalised by focus distance so a landscape shot is not a macro, with taps rejected
unless they are at least as out of focus as the pixel they blur into - which is what
stops a sharp foreground haloing into a blurred background. Between the scene and the
bloom, so a blurred highlight still blooms. Skipped whole when the aperture is shut.
400 frames in ordinary play: GL 7.1 s, VK 7.2 s - the lens does not draw there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>