Skip to content

Bake ground tiles from Lua into mipmapped arrays, hex tiled in the shader - #332

Merged
kcjonson merged 41 commits into
mainfrom
kj/terrain-edges-grass-textures-1cd28e
Nov 2, 2026
Merged

kcjonson merged 41 commits into
mainfrom
kj/terrain-edges-grass-textures-1cd28e

Conversation

@kcjonson

@kcjonson kcjonson commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Replaces the ground atlas with baked, mipmapped texture arrays. The faint tile-edge lines that flickered while panning came from the atlas: each surface's pattern was packed edge to edge with GL_LINEAR, no gutter and no mips, so every 1 m tile edge blended half a texel of its neighbor. That's gone at the source.

  • Ground defs (role ground, assets/world/ground/<Surface>/) drawn by Lua generators, ground_grass.lua and ground_bare.lua, 8 variants each, with path:setLod for per-element LOD.
  • GroundTileBaker renders each mip from the vectors (supersampled, Kaiser filtered, mean locked) into a GL_TEXTURE_2D_ARRAY (sRGB8_ALPHA8), cached on disk under <exe>/cache/ground/. Cold bake about 18 s on the splash, warm about 38 ms.
  • land.glsl does stochastic hex tiling at a constant mip per draw, one tap where a cell dominates, and macro tint from a per-chunk texture baked on the chunk worker.
  • Round 1 art for all nine land surfaces, and a Bake view in the asset-manager.
  • An in-game GPU hold for perf captures (WOR-553), so every sample lands at P0 without anyone locking clocks.

Perf on an RTX 3090, RelWithDebInfo, 3072x1728, quickstart planet, GPU held at P0; river, coast and desert views at seven zooms plus two scrolls, median of 9 samples. The tile pass is a median 0.04 ms under main, with the worst cell +0.015 ms against a noise floor of about 0.01.

Spec: docs/technical/ground-textures.md. The blending work is stacked on this in its own draft PR. Not for merge until I've tested it by hand.

tessellateAsset took its base index as a uint16 with no limit check, so an
asset over 65,535 vertices wrapped its indices and drew garbage without a
word. It now packs paths into as many meshes as it takes, in path order, and
AssetRegistry::buildMeshes hands them all back. buildMesh stays for callers
that draw one mesh (flora, groundcover, the asset-manager thumbnails) and now
fails with an error when the asset needs more than one, instead of
corrupting. A single path that tessellates past the limit on its own is
dropped with a warning, the way SVGLoader does.

GeneratedPath gains minPixelsPerMeter (0 = always drawn), and buildMeshes takes
a pixel density that drops the paths asking for more, which is what the ground
tile bake will use to leave fine detail out of coarse mips.
noise2(x, y, seed) is salal's gradientNoise2, about -1 to 1, so a generator
gets the same deterministic field the rest of the engine uses. periodicNoise2
(x, y, period, seed) wraps the integer lattice every `period` cells, so the
field tiles seamlessly (ground tiles need that); inside cells 0 to period - 2
it equals noise2 bit for bit. The period arrives as a double because P / F is
a float in Lua even when it divides evenly, and anything that isn't a whole
number >= 1 is a script error.

path:setLod(n) sets GeneratedPath::minPixelsPerMeter. Existing bindings are
unchanged.
A ground def (role ground, at assets/world/ground/<Surface>/<Surface>.xml) is
the tile art for one land surface. It names its <surface>, and optionally a
<period> in meters (default 4), <pixelsPerMeter> (64), <rotate> free, quarter
or none (free), <variantCount> (8 here, 1 elsewhere) and a <macro> with dry
and lush r,g,b tints, an amplitude and two wavelengths. The generator or
svgPath are the existing ones. assets already links world, so the surface is a
world::Surface, parsed with a new surfaceFromString next to surfaceToString;
AssetDefinition only forward-declares the enum, as PlacementExecutor.h does.

The validator checks that period divides 512 and fills whole pixels at the
chosen density, that variantCount is 1 to 32, pixelsPerMeter is a power of two
from 16 to 512, the surface is a land surface, that no two ground defs paint
the same surface, and that a ground def has no <biome> rules (placement
already skips defs without any). Once the tree has any ground def it warns for
each land surface that lacks one; that becomes an error when the content
lands. Ground-only elements on other roles warn.

Ground defs are skipped by the eager collision capture, which would otherwise
run their (heavy) generators at load for nothing.
… split

Validator rules for period, pixelsPerMeter, variantCount, surface, duplicates
and missing surfaces; schema parse and defaults; noise2 against salal's
gradientNoise2 and per seed; periodicNoise2 repeating along x, y and both,
including across the period seam and negative cells; setLod landing on the
path; a 22,000 triangle asset splitting into two valid meshes with every
vertex kept; and the pixel density filter.
…sRGB encode, metrics

Every downsample is a Kaiser-windowed sinc (alpha 4, 3 texels each way) that wraps,
clamped to the min and max under its main lobe, then given the source's mean back:
the clamp alone brightened a coarse mip of small light detail by 25%. The mean lock
adds a 2-texel blur of the residual against the reference, so the mean moves and
surviving detail keeps its contrast. Encoding dithers half a code so flat far mips
keep their mean.
GroundTileBaker draws each mip from the def's meshes at that mip's texel density, so
setLod paths leave at the level their author chose. A triangle's wrap copies follow
it, which keeps the generator's paint order at the edges. Mips 0 and 1 render at 8x8,
the rest at 4x4, tiled past 4096 px, then go through the Kaiser downsample, the mean
lock against mip 0 and sRGB encoding. It returns CPU chains plus per-mip metrics
(mean, delta E, variant spread, windowed contrast) and timings, logs the table, and
puts the coordinate system, viewport and bound target back.
Every level costs one full generator run whatever survives the LOD cut, about 95 ms
a level on the snow placeholder, which the per-mip column makes plain.
A ground def's detail pane gets a Vectors / Bake switch. Bake runs the game's
GroundTileBaker one variant a frame and shows the variant's mips at one texel a pixel,
a strip of what the game draws at 160 down to 3 px/m (a 2x2 of four variants, one LOD a
draw with the +0.25 bias, trilinear in linear light, computed on the CPU since salal
textures clamp), and the metrics table. Next variant steps through the chain; Reload
drops the cached bakes so the def bakes again.
Drawing a tile's whole mesh 3x3 doesn't wrap it: each copy's full-tile base
paints over the overhang of the copies drawn before it, and its own overhang
lands on top of everything drawn after, so blades get cut at two edges and float
over the other two. buildMeshes now repeats every path of a procedural ground
def that crosses an edge of its [0, period] square at the offsets it crosses
into, right after the original, so paint order holds across the seam. Clipped
to the square, the meshes are the seamless tile, which means the baker draws
them once instead of 3x3; the spec says so now.
A dense ground def needs several meshes, and buildMesh refuses it, so the asset
render path takes them all from buildMeshes. A ground def is framed on its
[0, period] square rather than its mesh bounds, and drawn clipped to it, since
its art crosses the edges. The detail pane shows the first variant tiled 3x3,
each copy clipped to its own cell, beside the usual variant grid, so a seam
would show. asset-cli's render smoke pass uses buildMeshes too, so a big ground
def isn't reported as producing nothing.

Drawing a ground def's 15 copies is slow in Debug, about 2 s a frame with the
grass tile selected.
ground_grass.lua paints one 4 m tile dark to light: a far stack for the coarse
mips (mean base, soil, clover and tuft proxies), a gap-shadow cover tagged
setLod(20), then soil peeks with pebbles, dark undergrowth fans, two-tone tuft
fans grown in clumps that share a tone and a lean and shade like a dome from
above, broadleaf rosettes, clover in Thomas clusters, and top blades with
centered underlays, broad blades, straw and dry tips. Density fields are
periodicNoise2 at 0.8 and 0.4 m, counts are per square meter, and every look
knob is a param, so the other grass surfaces can reuse the script.

Ground_Grass is the Grass surface's def with the tuned params. Mean is about
sRGB (73, 103, 49), the eight variants agree within 0.6% in luma, and the far
stack lands within 0.1% of the full-detail mean. In RelWithDebInfo a variant
is about 190 ms of Lua and 340 ms with wrap and tessellation, 27.7k paths and
230k vertices.
…ures-1cd28e

# Conflicts:
#	apps/asset-manager/AssetDetailView.h
buildMeshes repeats every ground path that crosses the tile edge right after the original, so the baker no longer emits 3x3 copies of each triangle. It scales the meshes into render pixels, drops what misses the target, and draws them in paint order; the target's clip is the wrap.
buildPaths runs the generator once and tessellates each path once, tagged
with its setLod density, and TessellatedPaths::meshesAt packs the paths a
density keeps into the meshes buildMeshes returned there. buildMeshes is
now a thin call over the two, and tessellateAsset is gone. The registry
also resolves a def's script path for callers, and remembers which XML
file a def came from.
…kes on disk

The baker builds every variant's paths at once on the TaskPool, one
generator run and one tessellation each, then draws each mip from them.
Each def's bake is kept in cache/ground beside the exe, keyed by its XML,
its script, its shape and the baker's version, so an unchanged def comes
back in about 20 ms. Ground_Grass in RelWithDebInfo goes from 19.6 s to
5.8 s cold; what's left is the GPU finishing salal's batched draws.

The asset-manager's Bake view takes the same steps, the paths in one
frame and a variant's render a frame, and Reload bakes a def again once
its files change.
GrassShort, GrassMeadow, GrassTall, Dirt, Sand, Mud, Rock and Snow get placeholder defs that share
ground_placeholder.lua: a flat base in the surface's old color under a light, low-contrast speckle,
4 variants at 64 px/m. Real art replaces them later. With all nine covered, the validator's
missing-surface warning is now an error; the ground tests build a full tree so they still check
one def at a time.
GroundTileBank bakes every ground def with GroundTileBaker once the registry has loaded, a variant a
call so the splash keeps drawing (its boot log counts them; the loading scene finishes the bake after
a --scene jump), and uploads them into GL_SRGB8_ALPHA8 texture arrays, one per layer size, every mip,
with a per-surface table of layers, period, density, rotation and macro tints.

land.glsl samples them on a chunk-continuous hex lattice (4 m columns, 148 rows a chunk): per-cell
variant, rotation and offset, Mikkelsen's luminance-weighted blend with a gain that relaxes with zoom,
one sample where a cell holds 98% or below terrain/ground/hexMinPxPerMeter, textureLod at a per-surface
LOD the CPU computes once a frame. The fringe's other surface takes the dominant cell alone. Samples
blend in linear light and are encoded back to sRGB for the rest of the pass.

The chunk worker fills a 257x257 macro texture at 2 m a texel, uploaded with the render tiles: a dry
to lush mix at 36 and 14 m and value noise at 10 and 5 m. Its last row and column are the next
chunk's first, so borders match, and each lattice corner is hashed once a chunk (3 ms a chunk).

Deleted: the tile atlas (GameBootstrap's initializeTileAtlas and surfacePatternPath), the old pattern
SVGs in assets/tiles/surfaces, and Chunk::getSurfaceColor and getBiomeColor.
The near stack now runs down to mip 3, so the default 24 px/m view (two
thirds mip 3) shows real clumps: tufts around a lit crown, light cores,
dark ground between them, clover and the odd soil peek. The far stack
for mips 4 to 6 is a coarse tone model of the near stack averaged into
one rect a mip 4 texel, so it keeps mip 0's mean and patches in the same
places instead of bright clover and tuft proxies.

Blades take the model's tone where they lie, over a proxy strip of that
tone kept to mip 3, so a light blade and its dark underlay average to
what the coarser mips show. Clump sizes and tones are normalized per
variant. The mean lock is under 2.3% on every mip (it was 8 to 17%),
mips 4 to 6 sit at 72 to 94% contrast, and the palette leans a little
more olive.
…res-1cd28e

The baker builds every variant's paths once on the pool, bakes a variant from them on the GL thread, and keeps each def's bake on disk under cache/ground beside the exe.
…-1cd28e

The bank now bakes through the staged baker, so there's one baker API and
the game reads the same kind of disk cache the asset-manager does. Each step
uploads every def in a row the cache holds, and at the first it doesn't,
builds that def's paths on the pool; later steps bake and upload a variant
each, and the last one stores the def. Cold, the loading scene's bake takes
4.5 s of steps; warm, 36 ms.
Each hex cell flips left to right or not off a hash bit, with its random uv offset and no rotation, so art with an up (grass blades) still breaks repetition without spinning sideways.
The camera is a hair off top-down, so the tile is drawn that way now: tufts are upright fans
rooted at their base, blades lean within about 30 degrees of vertical, and everything standing
is painted back to front by root. Shapes crossing the top or bottom edge are split there, so the
depth order holds across the wrap.

Undergrowth goes where a cover grid says the ground would show, until about a tenth of the tile
is left open, so every variant shows the same 15-20% of earthy soil and litter at close zoom.
Blade proxies take the blade's own average, the far stack is pinned to the measured mip 0 mean,
and the tile doesn't rotate.
On light views the driver drops the 3090 to P3 or P2 and tileGpuMs then
measures the clock, not the code (WOR-553). GpuHold draws a throwaway
shader into its own 512x512 target after the scene's GPU timer closes,
so no scene timer counts it and the frame is untouched, and sizes that
work each frame so scene plus hold GPU time sits near 0.8 of the frame.
Off by default, toggled with /api/control?action=gpuhold&value=0|1 and
read back from /api/state?what=gpuhold, so only builds with the dev
server can turn it on.

perf-capture.ps1 turns it on for the whole capture (-NoGpuHold skips it),
records the P-state and clocks per sample from a read-only nvidia-smi
query, and warns in red when a sample isn't P0.
ground-textures.md describes how captures hold P0 now (WOR-553), with
the on and off numbers for main and this branch, and the testing README
lists the gpuhold control and state view. The terrain spec's perf section
points at the hold instead of nvidia-smi -lgc.
The tile pass is short of quarter-rate work (int/float conversions and
MUFU ops), and the hex sampler added a dozen conversions a pixel. Cell ids
stay floats and cross to int through the 1.5 * 2^23 bias, the variant pick
rounds in float on the hash's low 12 bits instead of an integer modulo by
a uniform (the same pick for every power-of-two variant count), and the CPU
folds the chunk's cell origin and the macro tint and value constants.

River view: tileGpuMs down 0.01 to 0.02 ms in every cell.
Each inner tile's 3x3 binomial sum comes from its rows' 1 2 1 sums, read
from a nibble table, and the thin-feature test runs on the whole 4x4 mask
at once. The sums, and so the field, are bit-identical to the per-bit
version and to SurfaceField.cpp. The distance to an isoline takes one
inversesqrt in place of length() and a divide.

tileGpuMs on the river view drops 0.02 to 0.07 ms against the previous
commit in every cell; with both commits, coast and desert drop 0.03 to
0.09 ms against e533e5b.
…urface

Render tiles carry a near-interior bit (every tile within two is one
surface) beside the interior bit. In such a tile the fine warp term moves
the read at most its amplitude from where the low term puts it, so when that
range keeps the read's lattice cell at -1 or 0 the block is one surface and
the field is 1. evaluateSurfaceField and land.glsl both check that after the
low noise and skip the fine noise and the block; surface, field and margin
are unchanged (a new test holds the shortcut to the full evaluation), only
the reported warp is zero there. The golden test leaves the warp uncompared
where the shortcut's bound falls within rounding on one side only.

tileGpuMs against the previous commit: 0.03 to 0.06 ms lower at zoom 20
and 8, up to 0.02 lower elsewhere.
Under open water the shading mixes the ground out by 1, so the land field
and its ground samples there were thrown away. The tile pass now works out
the pixel's water coverage first (waterPixel: the distance field, its
wobble, the coverage) and calls landColor, from one place, only where some
ground shows; shadeWater takes that coverage and the ground. The picture is
unchanged.

tileGpuMs against the previous commit: river 0.10 to 0.24 ms lower at zoom
3 to 20 and up to 0.03 lower further out; coast up to 0.07 lower idle and
0.06 to 0.11 lower on both scrolls over the sea; desert, with little
water, within 0.01.
At 12 px/m (zoom 1.5) the one-sample ground looks the same as the blend in
screenshots: the hard cell edges it leaves are a few levels at most in
speckled art and don't read at 1:1. Starting at 16 lines the blend up with
the gain ramp, which is off at 16 and under, and saves 0.05 to 0.10 ms of
tileGpuMs at zoom 1.5.
Every hex pixel now samples its heaviest cell through the same
instructions, and only the transition bands branch for the second and
third. Before, the one-sample cases returned from three separate copies of
the sample and the bands sampled from three more, so a warp straddling a
band edge ran several. The weights, the cells and the blend are the same;
the blend sums in weight order now, a rounding-level difference.

The desert scroll at zoom 3, the one cell still over the gate, drops from
0.25 to 0.24 ms of tileGpuMs; other cells move within 0.01.
ground-textures.md gets the attribution, the changes with what each saved,
the final two-run table against main and what was tried and dropped, and
its Shader, render tiles, land field and tunables sections catch up with
the near-interior bit, the 16 px/m hex cutoff and the water skip. D16 in
the terrain spec gets the near-interior bit and shortcut, and status.md
the task.
The dither leaves each texel's rounding unbiased, but a small level's
errors don't average out: on a 4x4 mip 6 one code of a dark channel is
several percent of its light, so a dark ground tile (Mud, Dirt) missed the
bake's 0.5% mean gate at mips 5 and 6 on dither luck alone. Now a channel
whose linear mean lands more than 0.05% off has its codes stepped one at a
time, the texels that rounded furthest the wrong way first, until no step
brings the mean closer. Big levels are already within that and untouched;
every bake's mean error is now under 0.1% at every mip.

The baker's output changes for the same inputs, so its cache version goes
to 2 and every cached bake is dropped once.
Reading the cached bakes is most of a warm bake, and with real art for all
nine surfaces there are 56 MiB of them: one after another the lookups took
76 to 78 ms, 13 to 14 ms for each grass def's 11 MB file, and the warm
splash bake 90 to 93 ms. The bank's first step now looks every def up at
once on its pool and later steps take the result in order, so the lookups
take 20 to 21 ms and a warm bake 35 to 38 ms wall clock.
The three remaining grass surfaces get real ground art from
ground_grass.lua, each with its own params at 128 px/m and 8 variants,
mirrored like Grass:

- GrassShort is dry short turf, an olive sward dotted with tussocks of short
  yellow-green tufts, more straw, dry tips and soil peeks.
- GrassMeadow is a lush, saturated sward with more clover, almost no open
  ground and wildflowers.
- GrassTall is long, dark grass, bigger clumps of taller tufts and long
  flopping blades, some with tan seed-head tips.

Wildflowers are the generator's one addition: heads in blue-noise clusters
and singles, petal colors taken in turn so every variant has the same mix,
each on a proxy disc in its average color kept to mip 3, drawn over
everything and painted into the far stack's tone model. Off by default, so
Grass bakes as before.

Each def's far stack is calibrated from its bake (farMean, farContrast,
farCoarseContrast): every check passes but the mips 1 to 3 contrast flags
Grass shows too, and baked alone each is at or under Grass's cold bake.
ground_bare.lua replaces the placeholder speckle for the five unvegetated
surfaces, each def differing only in params, at 64 px/m and 8 variants:
Dirt is a crumble of clods with stones, cracks and dead grass bits, Sand
meandering wind ripples with shells and pebbles, Mud churned wet lumps with
puddles in hollows, Rock weathered slabs along wandering joints with
lichen, and Snow wind-sculpted drifts with blue lees and sparkle.

Everything is lit from the top of the screen, so every def is mirrored. The
ground (base, tone patches, rock plates) is laid out first into a tone
grid that the layers above take their colors from, so they keep the
patches' contrast at every mip, scaled so each variant averages to its base
color. Mips 4 to 6 are that grid as one rect a mip 4 texel, with puddles and
lichen averaged in, set from the bake report (farContrast,
farCoarseContrast, farGain) the way Grass's far stack is. What drops out at
the coarser mips is balanced so the mean lock has little to do: paired light
and dark grain, crests whose lit strip cancels their lee, lumps whose cap
cancels their darkened body, stones on proxies.

Every check passes but a few mips 1 to 3 contrast flags like Grass's. The
placeholder generator had nothing else using it and is gone.
ground-textures.md's art direction now covers each surface: its look, its
rotation mode and why, its tile mean and macro tints, and what blending has
to know about it (Mud's glossy puddles want a hard edge, Sand's ripples and
Snow's drifts break at hex cell borders). The content table names the two
generators, the baker and bank notes cover the mean-keeping encode and the
side-by-side cache lookups, and the bake and memory numbers are today's:
18.3 s cold and 35 to 38 ms warm, 56.0 MiB of arrays. status.md ticks the
content tasks and adds round 2.
@kcjonson
kcjonson marked this pull request as ready for review November 2, 2026 18:32
Copilot AI balanced review requested due to automatic review settings November 2, 2026 18:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Validation accepts oversized ground layers and unsupported SVG ground assets, allowing excessive GPU allocations and startup-blocking bake failures.

4 open issues
What changed in this PR

Replaces World-sim’s ground atlas with baked, mipmapped texture arrays to eliminate atlas-edge bleeding and improve ground rendering across zoom levels.

Changes:

  • Adds ground asset definitions, reusable tessellated paths, bake/cache interfaces, and asset-manager previews.
  • Integrates ground arrays, per-chunk macro tint textures, and surface-field shortcuts into world rendering and startup.
  • Adds GPU-hold controls for performance captures and updates tests and documentation.
File Description
scripts/​perf-capture.ps1 Enables GPU hold and records clock states.
libs/​world/​world/​chunk/​SurfaceField.test.cpp Tests near-interior flags and shortcut equivalence.
libs/​world/​world/​chunk/​SurfaceField.h Defines near-interior bounds.
libs/​world/​world/​chunk/​SurfaceField.cpp Adds near-interior evaluation shortcuts.
libs/​world/​world/​chunk/​RenderTiles.h Adds the near-interior flag.
libs/​world/​world/​chunk/​GroundMacro.test.cpp Tests shared macro-texture borders.
libs/​world/​world/​chunk/​GroundMacro.h Defines per-chunk macro texture data.
libs/​world/​world/​chunk/​GroundMacro.cpp Generates seeded macro tint fields.
libs/​world/​world/​chunk/​Chunk.h Stores macro textures and adds surface parsing.
libs/​world/​world/​chunk/​Chunk.cpp Generates macro textures; removes old color helpers.
libs/​world/​CMakeLists.txt Builds macro texture generation.
libs/​world-view/​world/​rendering/​SurfaceFieldGolden.test.cpp Adjusts CPU/GPU comparisons for shortcuts.
libs/​world-view/​world/​rendering/​shaders/​tile.frag Skips ground shading beneath opaque water.
libs/​world-view/​world/​rendering/​shaders/​includes/​water.glsl Separates water coverage from shading.
libs/​world-view/​world/​rendering/​GroundTileFilter.h Defines filtering, color, and sampling interfaces.
libs/​world-view/​world/​rendering/​GroundTileCache.test.cpp Tests cache round trips and invalidation.
libs/​world-view/​world/​rendering/​GroundTileCache.h Defines persistent bake caching.
libs/​world-view/​world/​rendering/​GroundTileBank.h Defines texture-array ownership and staged baking.
libs/​world-view/​world/​rendering/​ChunkRenderer.h Adds ground-array and macro-texture integration.
libs/​world-view/​CMakeLists.txt Builds ground baking components.
libs/​assets/​CMakeLists.txt Builds reusable tessellated paths.
libs/​assets/​assets/​TessellatedPaths.h Defines density-tagged path storage.
libs/​assets/​assets/​TessellatedPaths.cpp Tessellates paths and packs bounded meshes.
libs/​assets/​assets/​lua/​LuaEngine.cpp Exposes path LOD and seeded noise functions.
libs/​assets/​assets/​IAssetGenerator.h Adds per-path detail thresholds.
libs/​assets/​assets/​AssetValidator.test.cpp Adds ground-definition validation cases.
libs/​assets/​assets/​AssetValidator.cpp Validates ground roles and settings.
libs/​assets/​assets/​AssetRenderer.h Supports multi-mesh, clipped previews.
libs/​assets/​assets/​AssetRenderer.cpp Frames and renders ground tile previews.
libs/​assets/​assets/​AssetRegistry.h Exposes multi-mesh and reusable-path APIs.
libs/​assets/​assets/​AssetDefinition.h Adds ground roles and rendering parameters.
docs/​testing/​scenarios/​06-asset-manager-browse.md Adds bake and cache verification steps.
docs/​testing/​README.md Documents GPU-hold capture controls.
docs/​technical/​vector-graphics/​INDEX.md Updates the ground-art workflow.
docs/​technical/​organic-terrain/​terrain-polygons-architecture.md Documents shortcuts and clock-state measurements.
docs/​technical/​INDEX.md Updates the ground-texture documentation entry.
docs/​technical/​asset-manager/​README.md Includes ground tiles in browser coverage.
assets/​world/​ground/​Snow/​Snow.xml Defines procedural snow ground art.
assets/​world/​ground/​Sand/​Sand.xml Defines procedural sand ground art.
assets/​world/​ground/​Rock/​Rock.xml Defines procedural rock ground art.
assets/​world/​ground/​Mud/​Mud.xml Defines procedural mud ground art.
assets/​world/​ground/​GrassTall/​GrassTall.xml Defines tall-grass ground art.
assets/​world/​ground/​GrassShort/​GrassShort.xml Defines short-grass ground art.
assets/​world/​ground/​GrassMeadow/​GrassMeadow.xml Defines meadow ground art.
assets/​world/​ground/​Grass/​Grass.xml Defines grass ground art and parameters.
assets/​world/​ground/​Dirt/​Dirt.xml Defines procedural dirt ground art.
assets/​tiles/​surfaces/​GrassTall/​pattern.svg Removes the old tall-grass atlas pattern.
assets/​tiles/​surfaces/​GrassShort/​pattern.svg Removes the old short-grass atlas pattern.
assets/​tiles/​surfaces/​GrassMeadow/​pattern.svg Removes the old meadow atlas pattern.
assets/​tiles/​surfaces/​Grass/​pattern.svg Removes the old grass atlas pattern.
assets/​tiles/​surfaces/​Dirt/​pattern.svg Removes the old dirt atlas pattern.
apps/​world-sim/​scenes/​splash/​SplashScene.cpp Bakes ground tiles before enabling entry.
apps/​world-sim/​scenes/​loading/​GameLoadingScene.cpp Handles baking when startup skips the splash.
apps/​world-sim/​scenes/​game/​GameScene.cpp Integrates ground tiles and GPU-hold controls.
apps/​world-sim/​scenes/​game/​dev/​GpuHold.h Defines the GPU-hold controller interface.
apps/​world-sim/​scenes/​game/​dev/​GpuHold.cpp Implements adjustable offscreen GPU work.
apps/​world-sim/​Main.cpp Passes the app into resource initialization.
apps/​world-sim/​GameBootstrap.h Exposes ground-bank lifecycle functions.
apps/​world-sim/​GameBootstrap.cpp Replaces atlas bootstrap with the ground bank.
apps/​world-sim/​CMakeLists.txt Builds GPU-hold support.
apps/​asset-manager/​TileImageStrip.h Defines captioned bake-image strips.
apps/​asset-manager/​TileImageStrip.cpp Uploads and draws bake-image strips.
apps/​asset-manager/​scenes/​BrowserScene.cpp Wires bake previews and cache reloads.
apps/​asset-manager/​GroundBakeView.h Defines the staged bake inspector.
apps/​asset-manager/​CMakeLists.txt Builds bake-preview components.
apps/​asset-manager/​AssetThumbnail.h Supports repeated, clipped ground previews.
apps/​asset-manager/​AssetDetailView.h Adds ground preview and bake-mode state.
apps/​asset-manager/​AssetDetailView.cpp Adds Vectors/Bake switching and tiled previews.
apps/​asset-cli/​Main.cpp Uses multi-mesh generation for smoke checks.

Review effort: Balanced

Comment thread libs/assets/assets/AssetValidator.cpp Resolved
Comment thread libs/assets/assets/AssetValidator.cpp Resolved
Comment thread libs/assets/assets/lua/LuaEngine.cpp Resolved
Comment thread scripts/perf-capture.ps1 Resolved
- One layer-size rule (GroundLayer.h: period x pixelsPerMeter a whole number of texels, 1 to 1024) shared by AssetValidator, GroundTileBaker::begin and GroundTileBank::begin, so an 8192 texel layer is rejected at validation and the bank allocates no array for it.
- Validation reports an SVG (assetType simple) ground def as an error; the baker only runs generators. The spec says so.
- periodicNoise2 wraps the lattice index without overflowing int32 for periods near INT32_MAX.
- perf-capture.ps1's cleanup resets the pan too, so a failed scroll scenario doesn't leave the camera moving.
@kcjonson

kcjonson commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

This lands through #333, which now targets main and carries all of these commits plus the review fixes. Merging #333 closes this one too.

@kcjonson
kcjonson merged commit 68439c4 into main Nov 2, 2026
3 checks passed
@kcjonson
kcjonson deleted the kj/terrain-edges-grass-textures-1cd28e branch November 2, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants