Releases / v0.91.0
v0.91.0 In The Picture
2026-09-11 Engine
A relay's inbox is sized for the players it serves and says so at startup, the
relay bench measures the shape a real lobby produces and refuses to print a
ceiling off a failing step, floptle shot draws the game's UI so a menu can be
looked at from a terminal, and a shader knob on one part of a model can be
driven from a script.
A relay's inbox is sized for the players it serves
A relay on a small box was dropping packets at a hundred players while its link sat under one percent used, its forwarding loop at a millisecond and its CPU idle. The limit was not the machine: a UDP socket left at the kernel's default receive buffer holds a couple of hundred datagrams, and one synchronised burst from a hundred clients filled it. The only symptom was retransmit latency that reads as "the internet".
The relay now asks for an 8 MiB buffer, receive and send, and prints what it
was actually given on its first line of output — because the kernel silently
caps the ask at net.core.rmem_max, and on a stock Linux box that cap is a
fraction of the ask:
socket buffers: asked 8388608 B each; kernel reports rx 8388608 B, tx 8388608 B
floptle-relay: ⚠ socket buffer CLAMPED below the 8388608 B asked — raise
net.core.rmem_max … and net.core.wmem_max … with sysctl, then restart
The sysctl is the operator's half; the relay is the only thing that knows it
was clamped, so it says which one to raise and to what. Linux reports double
what it applied, which is why an unclamped ask reads as sixteen megabytes here
and in ss -m; the line spells out the usable half so it reads honestly beside
the ask. A bigger machine with the same socket drops at the same point, so this
is the fix and a bigger box is not.
Measuring a relay tells the truth about a failing step
floptle-relay-bench drives synthetic lobbies through a relay and reports what
a player costs. Three things about the first walk of that curve were wrong, and
each is fixed.
The host fans out. A game host tells every player what the others did, so
the bench's host now sends each ping to every other member of its lobby, not
only back to its sender — --lobby-size changes what the relay carries, not
just how many lobbies there are. --echo-only keeps the lighter shape.
A ceiling is printed only on a clean step. The old report printed its
highest ceiling on the step where the relay was dropping: stalled senders
lowered the rate, which lowered the per-player figure, which raised the
arithmetic. A step with anything unreturned, or a send rate short of the one
asked for, now prints implied ceiling withheld and marks its per-player cost
as not one. The report also shows how many pings were sent against how many
the rate asked for, so a stalling relay shows as a shortfall before it shows as
loss.
Bytes are called bytes. The relay's usage report named its throughput
egress_bps and ingress_bps and counted bytes. The fields are
egress_bytes_per_sec and ingress_bytes_per_sec now, before the first
reader applied eight the wrong way.
Every simulated player is a socket on the machine driving the bench; the help
and the docs now say to raise ulimit -n before a large run, because the
default limit fails a join past about 400 players with an error that reads
like a relay fault and is not one.
floptle shot draws the UI
floptle shot is how a project is looked at from a terminal, and it drew the
world alone: no UI pass, so a scene whose whole point is a screen — a main
menu, a character creator, a dialogue box, a HUD — came out as its backdrop.
Every enabled UI layer is now in the picture, drawn the way the Game view draws
it: world canvases in the scene, screen-space layers over the finished frame in
their z order at the scale their scale mode gives --size.
Elements a script builds with ui.make in start exist only once the project
has played, so they need --after; hand-authored elements are drawn as
authored. --no-ui gives the world alone, for a lighting comparison or a GI
preview.
node:uiRect() under floptle run used to answer 0, 0, 0, 0, which reads
like a measurement and is not one — nothing lays out without a surface. It
answers nil there now, as the reference always said it did; guard the call
(if x then) in anything that also runs headless, and use shot to see a
screen.
A shader knob on one part of a model
A model's parts can each wear a .flsl shader — skin on the head, a face decal,
a cloth overlay on the torso — with every uniform authored in the scene and not
one of them changeable at runtime: node:setShaderParam writes the node's own
Material, which on such a model usually does not exist, and the part handle had
no spelling for a uniform at all. The handle has one now:
local head = node:material("Head#2")
head:setShaderTexture("face", "faces/02.png") -- the character creator's face swap
head:setShaderParam("blush", 0.6)
if head:shaderTexture("face") ~= "faces/02.png" then ... end -- reads back, same frame
node:material() with no name drives the node's own Material through the same
four calls. A shader write lands only on a part that already has an override
wearing a shader — it never creates one, because an override is a whole
material and a uniform must not be able to blank a part — and a write with
nowhere to land is said once in the Console rather than lost. The node-level
call on a model with part overrides and no node Material fans out to every
part that wears a shader.
Upgrading
If you run a relay, add net.core.rmem_max = 16777216 and net.core.wmem_max = 16777216 to a sysctl file on the box before restarting onto this version, and
read the relay's first line of output afterwards — the buffer is fixed when the
socket is created, and a relay restart ends every lobby on it, so pair the two.
If anything reads the relay's usage report, the two throughput fields are
renamed. If a script reads node:uiRect() and also runs under floptle run,
it gets nil there now. Nothing else needs a change.
Floptle Hub
Engine
The binaries are not signed yet. On macOS, right-click the app and pick Open the first time. On Windows, if SmartScreen appears, pick More info then Run anyway.