MapView’s terrain display lists — the one bolted to the player, the ones over another cache, and what bounds each. What is drawn on that ground is the ground-overlay half of world-3d.md.
| What | Where |
|---|---|
| Which ground is DRAWN — the terrain display list | MapView.MapRaster (private inner) → the public Terrain and its Grid main/flavobjs. MapRaster.tick — the base default, which Terrain takes and every other raster overrides — sets area = Area(cc - view, cc + view + 1) around the player’s CUT (getcc().floor(tilesz).div(MCache.cutsz), view = 2, cutsz = 25×25 tiles) ⇒ the drawn terrain is only ~50–75 tiles across from you; Grid.tick then adds/removes one scene slot per cut, keyed in Grid.cuts, which therefore holds a cut exactly while that cut’s mesh is in the scene. ⚠️ That is a GRID smaller than MCache.grids: map data is dropped only when the server says so (invalblob type 1 → MCache.trim, which has no caller inside the client), so “the grid is loaded” is true well past the visible edge — test cuts when the claim is about what the player SEES. Fork: MapView.grounddrawn(Coord2d) (// addon:) + a groundChanged() tap at Grid.tick’s two mutation points, which is how a client-only gob stops drawing over the void. That tap is gated on MapRaster.liveground(), true by default and overridable: grounddrawn reads terrain.main.cuts alone, so a raster whose cuts are not that ground wakes the addon layer’s placement pass for a set it never reads |
| From a grid’s arrival to the first drawn frame (fork) | The black “Waiting for…” screen is MapView.draw catching the Loading that camera.tick → getcc() → Gob.getc → MCache.getz(SurfaceID, …) → Grid.getcut throws while the cut under the player has no mesh — its text is that cut’s last stumble (Defer.NotDoneException carries Future.lastload, cleared only on completion), so it names a resource long since loaded; the DURATION is the wait for that one mesh, then the first drawn frame. Measured at login (1782–1797 ms of black, then a first frame of ~1.1 s) and taken apart: MCache.Grid.fill → Cut.invalidate() is lazy — Deferred.invalidate() drops the future in flight and clears inited (volatile: get()’s unlocked fast path tests it) so the next get() rebuilds; it used to rebuild() at once, scheduling a future for every cut of every grid the server sent (288 for nine grids, before the view had asked for its 25) and the first get() then scheduled each wanted cut again, so every cut on screen was built twice and the one under the camera queued behind the flood. The cut under the player is urgent from whoever asks first — Gob.getc for Glob.plgob, a Loader thread placing the player’s own gob at login, before the camera has ticked — and the pool holds ordinary work while it is outstanding (boot-and-loop.md, the urgent lane); its texture chain is discovered a stumble at a time (tileset → -tex → transitions-* → ridges/* → the paving -texs under the character), each restart a full rebuild, which is what the login warm-up pre-loads. The black frames do the first frame’s setup: MapView.draw runs PView.ensureenv (the instancer attached to an EMPTY tree — attached on the first drawn frame it walked every slot added meanwhile, compiling each and blocking on each unprepared texture through RenderList.syncadd’s waitfor: 720–1170 ms) and lights() (the light-grid classes and shaders, and a light state set before the terrain’s slots, which a state set after recompiled every one of them), and updsmap makes the ShadowList/ShadowMap before asking getcc(), so shadow slots compile one by one as cuts arrive rather than as one walk on the first tick with a camera. A cut’s slot.add that throws for a texture boosts it to URGENT before the per-frame retry. ⚠️ Terrain.tick re-asks only the cuts in vis, and vis comes from cutvisible over a camera that has not ticked — a cut outside that guess sits rescheduled until the camera is right; reqarea’s 49 getcuts run after super.draw and never during the black screen. After all of it: black 300–550 ms, of which the grids’ own arrival ~130, a cut alone with warm code 19–50 ms, and the rest first-time class loading on the UI thread (OCache.ctick’s first pass over every gob, the widget flood the Loader applies under synchronized(ui), the first draw’s resource code) — the JVM’s cold start, not the terrain’s |
| A SECOND source of ground in one scene | Fork: MapView.RecallTerrain extends MapRaster over a second MCache filled from the map database (mapfile.md) instead of from the wire — the same pattern as MapView.SessionTerrain, which rasterizes another session’s MCache into this scene. What separates such a raster from Terrain: its area is centred on the camera (RTSCam.center(), or getcc() under every other camera, where the two are the same place), and is sized in whole GRIDS, because a grid is the unit its source reads. It decides what that source reads — RecallTerrain.tick builds the wanted grid set and MapView.recalltick hands it over to the source — so the one owner of that decision is the one that knows where the camera is pointing and what of that is on screen. What it holds is the set of grids RecallTerrain.grids is handed each tick by the far ground that draws the zoom grids (mapfile.md) — the level-one zoom cells close enough to the camera’s eye to be drawn whole in any view a turn of the camera about its centre gives, as many as a share of the heap holds, nearest first — on screen now or not, so a turn of the camera hands over the same set —, every cell within MapView.recallnear cuts of the live area, and the cells still drawn whole while what replaces them is built — and what it draws is the part of it RecallTerrain.shown names; the rest it builds and merges out of the scene. What the source fills is one grid wider: MapMesh.dotrans reads a tile across the cut edge and the corner heights need the same tile, so a cut at the fill’s own edge throws MCache.LoadingMap and never completes. What the source holds follows the record: a grid it holds that the record takes in again is read again and, where the ground changed, filled in place (AddonWidgets.putgrid), so each cut goes on drawing its old mesh until the new one is built and the grid is merged again as they land; a copy kept as read hands changed ground back as it was the moment the character walks away from it (mapfile.md, a copy read out of the record). It draws only grids the source is already holding (AddonWidgets.loadedGrid), never through a getcut that would ask for one. It yields every cut the live raster has in the scene (terrain.main.cuts), because both sources hold the same ground there and one mesh on its twin is z-fighting, not a merge — the same rule SessionTerrain obeys against the anchor — and it builds and holds the cuts it yields all the same. ⚠️ Yielding the live area instead, with none of it built, opened a hole on both edges of every step the character took: Terrain adds a cut only once its mesh is built (reqarea builds the ring one cut past view ahead of time) and drops one the moment it leaves area, so the recorded side let go of a cut the live side had not drawn yet, and needed a build to draw a cut the live side had just dropped; the sky showed through for a few frames. RecallTerrain.mergetick makes a grid whose live share changed again in that same tick, past its budget, and Terrain ticks first (MapView.tick), so a cut changes hands within one frame. Every grid it is handed is built, on screen or not, and the main draw list’s frustum culling leaves out what is out of view; its slot carries ShadowMap.maskshadow, so none of it is drawn into the shadow map. ⚠️ A grid left unbuilt for being out of view is drawn whole with nothing in it the moment a turning camera reaches it. And it answers liveground() false. Installed in basic under every camera while MapView.recallon is on; removed with it, and whenever the source cannot vouch for the offset its ground was read through. ⚠️ Whatever takes such a raster out of the scene has to zero what its tick published for anything outside it, in the same breath: MapRaster.Grid.tick returns at once while slot is null, and a MapView that stops calling the raster’s own tick is not calling it either, so a count or a wanted set left over from the last frame it was drawn stands for as long as it stays out. Grid.cuts is the one thing that empties itself (Grid.removed), which is what makes a stale figure beside it read as an honest zero. It is drawn in its tilesets’ own materials, so nothing tells it apart from live ground but that nothing stands on it. ⚠️ Such ground is far outside the shadow map’s box, and upstream’s ShadowMap.Shader.shcalc shaded everything outside that box as in shadow (render-gl.md) |
| Where such a raster’s settings live | Fork: MapView.recallon and MapView.recallrange — two static fields with like-named prefs, each written live-and-persisted in one statement the way MapView.invcamx is, so neither half can move without the other. They are static because the settings are the client’s: every session up draws its own remembered ground out of its own record, and a switch the user flips once has to move all of them. MapView.recallrange is the DRAWN reach in grids and the source is filled one grid wider for dotrans’s margin, so the raster’s own r is the field and the fill’s radius is the field plus one; both are read per tick, which is what makes a range change apply with nothing to rebuild and nothing to tell. MapView.recallrangemin/recallrangemax are the bounds, stated once and read by the Performance panel’s HSlider, the field’s own clamp and the Lua refusal; the slider starts at recallrangepanel, and a client with no stored range starts at recallrangedef. A third, MapView.loddist (pref loddist, loddistmin..loddistmax = 25..200 per cent, default loddistdef = 100), is the panel’s LOD distance under the range, and has no Lua option: recalltick hands the far ground loddist / 100, which divides both of its pixel limits (RecallLod.texelpx, fullpx), so every boundary of the chain — the grids drawn whole, then each coarser zoom level — stands that much nearer the camera or further from it, while the cells held whole around the live area (recallnear) stay whole. :terrainlod sets the two limits themselves, live and unpersisted. MapView.recallreach() is how far from its centre that ground can stand in world units, which DefaultCam.setproj holds its far plane past while recallon is on (camera.md). :recall in cmdmap takes no argument and is a report: MapView.recallgridsheld/recallgridsread/recallcutsdrawn/recallcutswanted (// addon:) are the four numbers it prints, and the same four an addon reads back |
| Bounding what such a raster DRAWS | Fork: MapMesh.build is two passes over 625 tiles plus dotrans’s eight neighbour reads each, then a slot compile and a VBO upload, on the Defer threads every loading thing in the client shares, and MapRaster has no budget of its own. Grid.tick calls getcut for every cut MapRaster.walk() names, in iteration order, and drops the slot of every cut MapRaster.holds refuses — area and area.contains by default (// addon:). A raster whose cuts are a sparse set overrides both: the recorded ground’s grids lie around the character and around where the camera looks, which can be a hundred grids apart, and the rectangle around both is a million cuts to walk every tick. RecallTerrain.tick takes every grid it is handed that the source holds (AddonWidgets.loadedGrid) whole, with no frustum test per cut: the ground is drawn a grid at a time (the next row), so a cut test would only change which cuts a visible grid is made of on every turn of the camera — a copy of the grid each time — and the main draw list’s frustum culling tests the grid’s mesh as one box instead. What is handed over is every grid some turn of the camera draws whole, and the tick runs every ctick, with no clock of its own — and does nothing at all when it finds what it was handed, the live cuts, MCache.chseq and the ground generation where they were and its last tick left nothing to do (RecallTerrain.settled; the click raster follows its idle), which is every tick of a camera standing or turning. A whole tick does work in proportion to what moved, never to what is held — thousands of cuts at a long view distance. The candidates (every cut of a grid handed over that the source holds and can build), kept by grid, are made again only when the grids handed over or the source’s MCache.chseq move, and draw with them (RecallTerrain.fullwalk); between two, only the cuts the build budget has room for are let in, from where the last tick left off (admit). main is visited over the cuts just let in, the ones whose last visit met a Loading, and the ones whose mesh has just been built — MCache.builtlog (// addon:) logs the cut of every finished build, and MCache.cutpending whether its new mesh has been taken up — and over every cut let in only when one was let go, the raster came into the scene, or chseq or the ground generation moved (RecallTerrain.maintick, over MapRaster.Grid.visit/prune/update, // addon:). What moved in main (MapRaster.Grid.changed, // addon:) is handed on a grid at a time (drain): to mergetick, which looks only at those grids and the ones still waiting; to RecallClick, which updates only those cuts; and to the held grids, kept as one set the source finds unchanged until a grid comes or goes. ⚠️ A cut invalidated by a refill or a ground setting goes on answering its OLD mesh from getcut until its rebuild lands, so a raster that stops asking never sees the new one; and MCache.meshbuilt counts a build inside build(), before the build’s future is done, so a visit on that tick is still answered the old mesh — a cut from the build log stays watched until cutpending clears. The cuts are ordered grid by grid, each grid nearest the camera’s own cut first and a grid’s cuts together, nearest first (RecallTerrain.sortgrids, sorted again only when the candidates or the camera’s cut move), so each grid comes whole, to be merged and drawn, before the next starts; the grids the far ground says the view wants whole (RecallLod.onscreen, handed over as RecallTerrain.onscreen) are let in before the rest, so the ground on screen comes before what is held for a turn of the camera, and a turning camera sorts nothing. draw is in the order its cuts were let in, and is what the raster walks. ⚠️ boxvisible(ul, br) tests a box at the character’s height, cc.z ± 100: ground far below or above it (the character on a hill, a tower) projects elsewhere on screen, so a camera zoomed in or tilted down drops boxes that are in view. ⚠️ A cut that can never build blocks every other one. Its mesh reads one tile past each edge (MapMesh’s Scan(-1, -1, sz + 2)), so a cut on the edge of explored ground reads into a grid the record never had and throws LoadingMap for good; counted as in flight it holds a slot of recallmaxbuild forever, and once the budget is full of them nothing else starts. RecallTerrain.buildable admits only a cut whose grids are all held. ⚠️ A cap derived from the frustum instead is not a cap: the camera can frame more ground than this client was built to draw |
| Drawing it a grid at a time (fork) | A cut holds a mesh per material its tiles are laid in (MapMesh.Model, one per MatKey, and a Tiler.SModel per material and vertex factory), a few hundred triangles each, and every one is a draw call: hundreds of recorded cuts on screen were most of the scene’s draw calls. MapMesh.parts() (// addon:) hands out each Model’s finished mesh with its material — never the Model itself (the gotcha on what a built cut keeps, below). RecallTerrain.main still holds every cut in the scene — the authority heldgrids, the budgets and RecallClick read — but its produce is RenderTree.Node.nil; RecallTerrain.mergetick draws each grid with cuts in main as the concatenation of those cuts’ meshes — all but the ones the live raster has in the scene — per equal material and equal vertex layout, each cut’s positions moved by where it stands in the grid, placed at the grid’s corner. That mesh keeps no copy of what it draws: its vertices and indices are written from the cuts’ own meshes straight into the buffers the GPU is filled from, when it is prepared and again if another environment ever asks (FastMesh(VertexBuf, int, Filler, Volume3f), // addon: — no index array of its own, and the box given, which the frustum test reads), so a grid drawn whole holds its data once, in its cuts. A grid is merged again when its set of cuts changes: at once when every cut draw gives it has arrived, else once the set has stood still for MERGEWAIT, at most MERGES grids a tick, the grids the view wants whole first, and as the frame’s upload budget lets it (io.brodgar.perf.UploadBudget, render-gl.md; never for a grid already drawn whole, which with no merge is a hole), and at once past both when the live raster has just taken a cut of it or given one back; a grid waiting draws its previous mesh, which owns its buffers and holds on to the cut meshes it was made from, rebuilt since or not. A grid let go keeps its merge out of the scene and on the GPU (RecallTerrain.park, up to PARKCAP, the oldest let go first), and one handed back with the very cut meshes it was made from is put back as it is (unpark): a zoom in lets go of hundreds of grids that the zoom back out wants again. A mesh whose material texture is not prepared (Loading on the add) is kept and added again next tick, not made anew. ⚠️ The indices are 16-bit: a group past 65,535 vertices starts another mesh. ⚠️ A mesh is placed by its slot’s Location, and a copy’s vertices are grid-local, so anything that reads object space (Homo3D.objv) would see the grid’s frame instead of the cut’s; the ground’s materials read mapv, which is the same either way |
| Bounding what it BUILDS, which is a different budget | Fork: MapView.recallmaxbuild is a concurrency target and not a quota per tick — Math.max(1, (Defer.maxthreads + 1) / 4), a quarter of the logical cores (boot-and-loop.md): cuts built side by side slow one another down about as much as they add (the cut under the character cost 47 ms built alone and 300–400 beside thirteen others, Defer.take), and the rest of the machine is the frame’s and the pool’s other users’. The priority a cut is asked for at is the raster’s (MapRaster.loadprio, // addon:, upstream’s 5 by default): a cut of a grid the view wants whole asks for 5, as the live terrain’s do, and one held ahead of the view — for a turn of the camera, for the live ground to walk into — for nothing, so it is built when nothing that asks is waiting. It is charged only for a cut that is in neither Grid.cuts nor MCache.cutbuilt (// addon:), so what it counts is what has been started and has not yet arrived, and a cut already built is admitted for free: fullwalk goes on past the budget (continue, never break), or a run of unbuilt cuts would stop it before the built ones behind them; admit, between two, stops at the first that does not fit, as after a fullwalk only this raster’s own builds make a cut built. Paired with a tick on the frame rather than on a period, throughput is that target over one build’s own latency instead of over a clock — a finished build is replaced within a frame, where a fifth of a second left a five-millisecond mesh’s slot in the budget idle for the rest of it |
Gotcha — a raster over a cache the client must not send for cannot use getcut alone. MCache.getcut
ends in getgrid, which on a miss calls request(gc) and throws LoadingMap — so a MapRaster walking an
area wider than its source has filled queues every absent grid in it. On the live cache that is the point; on
one filled from disk it fills a queue nothing may ever send, and buries the request count that is the only
evidence such a source is behaving. Ask the cache what it holds first (AddonWidgets.loadedGrid, // addon:)
and skip the cut when it holds nothing. That is also what drops a cut whose grid has just been disposed:
MapRaster.Grid.tick removes the slot of any cut its skipcut starts refusing, so the stale mesh leaves the
scene at the next tick instead of being drawn after its dispose().
Gotcha — a second source of ground is a second MCache, and only one of the two has a monitor anybody
else wants. AddonWidgets.loadedGrids(mc) (// addon:) copies a cache’s whole grid map and
AddonWidgets.loadedGrid(mc, gc) looks one coord up in it; both take mc.grids. On a raster’s own
disk-filled cache that monitor is taken by the raster and by whatever fills it and by nothing else, so a copy
there is merely wasteful. On sess.glob.map it is the monitor the map and the render threads take, which
world-3d.md already prices per cut. ⚠️ So a question about a handful of grids is asked by
coord, once per grid named — the same shape RecallTerrain.tick’s per-grid pre-reject already has.
Copying the session’s cache to answer one pays that monitor for every grid the session has ever loaded, and
that set only grows: MCache.trim has no caller inside the client. The copy is also the smaller half of what
it costs, because the walk over it is a Coord and a lookup for each of those grids, on every pass.
Gotcha — what such a source keeps is an LRU, and the raster’s own cuts is what it may never drop.
MCache.trim keeps a rectangle, and a rectangle cannot say keep what was built: one grid of travel puts a
whole rank of grids outside the square, so panning one way and back re-reads and re-meshes every one of them.
MCache.drop(Collection<Coord>) (// addon:) disposes exactly the grids it is handed instead, and it names
what goes rather than what stays — the keep set is decided on the UI thread while grids arrive from a
Defer thread, and naming what stays would dispose one read off the disk before anything could draw it.
⚠️ Order the two sides the right way round: an MCache.Grid disposes its cut meshes with itself and
MapRaster.Grid holds a scene slot per cut it has drawn, so the raster’s cut map is the authority over the
budget — a grid under one of its entries is kept whatever the order says, or its slot goes on drawing a
disposed mesh with nothing to notice. trimall has no next tick to notice either: it disposes every Grid at
once, and a raster removed in the same breath never ticks again, so the slot comes out first and the dispose
follows.
Gotcha — skipcut runs OUTSIDE Grid.tick’s Loading guard, and nothing above it catches one
either. Grid.tick wraps only the getcut half in try/catch(Loading); the skipcut test sits ahead
of that block, so a Loading out of it leaves tick altogether. There is no net under it: MapView.oltick
ticks its overlay rasters in a loop after its own catch, and MapView.tick calls oltick inside
synchronized(glob.map) with no guard of its own — so the throw takes the whole frame. A skipcut that
consults the cache (MCache.getgrid, Indir.get) must therefore swallow Loading and answer
conservatively: yield nothing, and let the getcut that follows throw where it is caught.
Gotcha — a cut’s MESH outlives the slot that drew it, so “not in Grid.cuts” never means “needs
building”. MapRaster.Grid.removed clears cuts when the raster leaves the tree, and Grid.tick drops
the slot of every cut skipcut refuses — neither touches MCache.Grid.Cut.mesh, a Deferred that holds
its built value until the whole Grid is disposed. So a raster that came out of the scene and went back in
finds every cut it had built still built, and Deferred.get hands it back with no Defer task at all;
MCache.numcuts counts exactly those (Cut.mesh.cur() != null) and MCache.cutbuilt (// addon:) asks it
of one cut, as a plain lookup that neither builds nor requests. ⚠️ A budget that reads absence from cuts as
“being built” therefore throttles work nobody is doing: it is the right test for what is IN FLIGHT only
while a dropped cut also loses its mesh, and a source that keeps its grids breaks that coupling — admit an
already-built cut for free and charge the budget for the rest.
Gotcha — a built cut keeps what laying it again reads, and each Model is let go of only if nothing holds
it. MapMesh.build ends in clean(), which drops every data entry whose ConsHooks.clean() answers
false — each Model, the MeshBuf its mesh was made from, every vertex, face and layer value of it an object —
and keeps the ones that answer true: MapSurface (every corner a Surface.Vertex, the corners of every face in
fv/tv, the normals), Ridges and WaterTile.BottomData. Those are what laying the cut’s tiles again reads:
makeol/makeolol (overlays), consgrid (grid lines), groundmod (ground decals) and the flavor pass
(Ridges.getrdesc). Measured over recorded grids, a built cut is some 700 KB under the lightest ground settings
and 1.7 MB under the defaults; of that, the kept surface and tile data is a quarter at the lightest and a tenth
at the defaults, and the Models are half to two thirds — if anything holds them. ⚠️ So hold a Model’s
finished mesh and material and never the Model: a list of Models keeps every MeshBuf for as long as the
cut lives, which is what MapMesh.parts() (// addon:) avoids. Fork: an MCache built lean
(MCache(Session, boolean), // addon:) has each cut let go of its data as soon as it is built
(MapMesh.lean) — the remembered ground’s, whose cuts are drawn through a merged mesh, clicked through flat
and stood on through getsurf(SurfaceID.map), whose surface reads the map’s heights and not the cut. ⚠️ Nothing
may lay a lean cut again: MapMesh.data throws IllegalStateException, so an overlay, grid lines, a decal, a
flavor pass or a water bottom’s surface (WaterTile.BottomSurface, a SurfaceID.trn) over such a cache fails
loudly instead of building over an unfinished surface.
getcut/getfo answers: the two Deferred halves, how one is rebuilt and swapped, the blend layers and the transition pass, and the flavor passarea is centred on, and the frustum that is not a capDefer pool a build budget is a share ofSessionTerrain, the same pattern over another session