Render started the day at 111 definitions. It is at 92. Twenty-one came out as four separate liftings, and the interesting half of this note is the ninety-two that stayed — because the reason they stayed is not that nobody tried.
The question was: how much of the safari's renderer is not about the safari? The honest answer is about a fifth, it does not come out in one piece, and two of the four pieces were not in Render's own sections at all.
Frame — the route walk and the coordinate change. Nine definitions: the
chain, the rider pose, the composition, the mappers. It is the piece a different
screensaver keeps whole.
compose-down : List Segment, List Integer, Integer, Real, Real -> AX
compose-down (segs) (ch) (k) (a) (x) =
if k <= 0 then AX { a = a, x = x }
else let seg = list-at segs (list-at ch (k - 1))
in let p = next-to-cur a x seg.length seg.exit-angle seg.exit-right seg.width
in compose-down segs ch (k - 1) p.a p.x
at : List Segment, List Integer, Pose, Integer, Real, Real -> RiderPt
at (segs) (ch) (pose) (d) (a) (x) =
let p = compose-down segs ch d a x
in to-rider p.a p.x pose.along pose.across pose.yaw pose.hw
Nothing in those nine mentions a tree, a tower, a cow, a cat, a rail or the
truck. And the four names other chapters already read from Render —
build-chain, at, prev-map, map-pt — were all in this half. The export
list and the dominator tree were answering the same question from opposite ends.
Billboards — a sprite placed in the rider's frame, and the two culls.
verdict : RiderPt, Real, Integer, Boolean -> Placed
verdict (rp) (h) (cp) (fr) =
if rp.forward <= near
then Placed { b = no-billboard, kept = False, size-culled = False }
else if h / rp.forward * focal < min-critter-px
then Placed { b = no-billboard, kept = False, size-culled = True }
else Placed { b = Billboard { right = rp.right, fwd = rp.forward,
height = h, cp = cp, face-right = fr },
kept = True, size-culled = False }
verdict does not know what the sprite is of, and it never did — every emoji
animal in the safari collects to the same Billboard, so the rule was written
once for all of them. The callers differ; the verdict does not. That is
exactly the shape that lifts.
It carries one detail a re-implementation would lose, so it is worth keeping in
view: a placement records its own verdict rather than being dropped, because the
near-plane cull is silent in the zig — it continues without touching a
counter — so a placement can be neither kept nor size-culled. That third
state has to survive, and a boolean would have eaten it.
Ground — a quad on the floor. Drop each corner by the curvature at its
distance, clip to the near plane, project, paint. Four definitions, and it does
not know whether it is painting a road, a corner's pavement or a pond.
ground-vert : RiderPt -> Vec3
ground-vert (p) =
Vec3 { right = p.right, forward = p.forward,
height = 0.0 - ground-drop p.right p.forward }
Ground quads never enter the depth sort — they are painted first, in walk order, and everything that stands up is sorted and painted over them. So this is a pipeline of its own rather than a stage of that one, which is why it detaches cleanly.
And two definitions that were simply in the wrong chapter. Those were not
found by looking at Render at all; they fell out of asking xref who where a
name lives.
project-all defined in GuardRail read by Render, TruckDraw, Ground
ceil-real defined in Render ceil x = -floor(-x)
project-all is Camera.project in a loop:
project-all : List Vec3, Real, Real, Integer -> List ScreenPt
project-all (ps) (cf) (view-w) (i) =
if i >= list-length ps then []
else [project (list-at ps i) cf view-w] & project-all ps cf view-w (i + 1)
It lived in GuardRail because that is where the first caller happened to be.
The consequence is the kind of thing nobody notices from inside: a rail
chapter was on the cite line of everything in the program that projects a
polygon. It is beside project now. ceil-real went to Num, where a
rounding identity belongs and where nothing about it is a road.
This is the one I expected to lift and could not. The sort itself is scene-independent to look at — a stable merge sort, far to near, with a carefully argued epsilon:
sort-tie : Real
sort-tie = 0.00000006
deeper-than : Real, Real -> Boolean
deeper-than (x) (y) = x - y > sort-tie * real-max (real-abs y) 1.0
sort-items : List Item -> List Item
sort-items (xs) =
if list-length xs <= 1 then xs
else merge-items (sort-items (list-take xs (list-length xs / 2)))
(sort-items (list-drop xs (list-length xs / 2))) 0 0
Then two declarations above it:
Kind =
| KTree
| KTower
| KCow
| KCat
| KTruck
| KRail
Item = record {
fwd : Real,
kind : Kind,
i : Integer
}
sort-items : List Item -> List Item, and Item names the six things a safari
contains. A night walk sorting lamp-posts and doorways would need the identical
algorithm and could not call this function.
The fix is not a move, it is a design change, and that is the honest reason
it stayed. Either Item becomes generic in its payload — which needs a type
parameter this code does not have and which the whole collect-then-merge design
would have to be rewritten around — or Kind becomes an Integer tag and the
compiler stops checking that a KRail index indexes the rail list. The second
is cheaper and worse. deeper-than and sort-tie are liftable on their own,
but a two-definition chapter holding one comparison is a filing decision
dressed as a design.
So: the sort is a place where Codex's type system is doing its job and the cost of that job is a reuse boundary. I would rather write that down than paper it over.
min-scenery-px : Real farm-seg-reach : Integer
min-critter-px : Real safari-seg-reach : Integer
max-vis-trees : Integer max-vis-towers : Integer
max-vis-critters : Integer max-vis-cats : Integer
min-critter-px went with Billboards because verdict reads it. The rest
are budgets for a specific bestiary, and the surrounding prose is emphatic
that the two culls are deliberately different and both must stay live. There is
no generic chapter hiding in a list of safari budgets.
collect is still 67 definitions of safariThe dominator tree has not changed its mind: collect owns 67 and
frame-ground owns 24, and the two sets remain disjoint. But every one of the
91 is about trees, towers, critters, the crossing cat, the guard rails, the
chased truck, the road strip or the pond. Render being big is not the same as
Render being confused, which is the reading TruckDraw earned earlier today —
one exported name owning sixty-four others — and Render is closer to that than
it looked.
Render 111 defs -> 92 defs
lifted 21 Frame 9, Billboards 7, Ground 4, ceil-real 1
rehomed 1 project-all, GuardRail -> Camera
Nineteen percent, in four pieces, none of them large. If the hope was a clean half, the code said no.
But the reusable set is not really "what came out of Render" — it is what the
tree already sorted itself into. Asking which chapters reach the drawing spine
(Camera, Frame, Paint, Blit, Lens) partitions the port 22 model to
11 view, with nothing straddling:
MODEL Cat Truck World Trees Herd Pigs Scenery Pond Rider Gaze Geom
Camera Lens Frame Num Trig Pose Stills CatStills EmojiStills
SafariCritter Ground Billboards
VIEW CatDraw TruckDraw Tree Tower GuardRail Critter Mountains Sky
Render Safari Blit
Camera, Lens, Frame, Geom, Ground, Billboards, Num, Trig,
Paint, Pose — that is the reusable set, and it is now ten chapters rather
than a fraction of one big one. The safari lives in the view column, and a
variation replaces that column wholesale.
Which is the answer to the original question, arrived at from the other side: not much of Render is non-safari, and that is fine, because Render is the view. The parts worth reusing were never going to be a slice of it. They were the chapters underneath, and the work was getting them out from under.
The obvious next thought is that Camera, Lens, Frame, Geom, Trig,
Num, Paint, Pose, Ground and Billboards should stop being
Safari chapter <X> and become a QUIRE of their own -- a fifth directory
beside port/, judge/, gold/ and poc/, registered in harness/bundle.py
the way those four are, and cited as Spine chapter Camera or whatever it ends
up called.
Steve's call, and it is the right one: not until the night walk exists.
The argument is the one about abstraction earning its keep on the second user.
Right now there is exactly one user of that set, and every judgement about
where its boundary runs is being made from a single example. Two of today's
five decisions would have been guesses under those conditions and both would
have been wrong in a way that is only visible from outside: project-all
looked like a guard-rail function until a third caller appeared, and Scenery
exists at all because the call graph proposed a cut the types refused. A quire
drawn now would freeze exactly that quality of judgement into a directory
boundary and a cite prefix, which is a much more expensive thing to move than a
chapter.
The night walk is the second user, and it is the only thing that can say which
of the ten are actually spine and which are safari-shaped by accident. Frame
is the obvious suspect -- it still cites World for Segment, and a route of
straight runs joined by turns is an assumption, not a universal. A city walk
either keeps that assumption or replaces World beneath it, and which of those
happens is a fact about a program nobody has written yet.
So the chapters get to be chapters for now. The partition is legible from
xref whenever anyone wants to see it, which is most of the value a directory
would have added, and none of the cost.
Every seam here is a seam in the CALL graph. Billboards and Ground are also
clean in the type graph — I checked by hand, which is the admission — but the
tool does not check it, and Scenery exists in the World split precisely
because the call graph proposed a cut that the types refused. Until the
dominator tree reads the type references the cross-chapter index already
collects, a proposed cut means "the calls permit this" and a human still has to
ask whether anything else objects.