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:

lua
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

Linux x86_64 7.9 MB Download → sha256 7611e55b636592f8c6cf0e1e990eee94fd5a1ee41e6d65d17f35b97f5295705e
macOS Apple Silicon 5.9 MB Download → sha256 567345ceb3afc704a719d159b7cf77f3463b187d712c6350bb8ccc11e2713f87
macOS Intel 6.4 MB Download → sha256 c2e237c6b8fb65d8c81f96c3524c6ad334d9fbaf492d20c056cbc9e3456de5ed
Windows x86_64 7.3 MB Download → sha256 7c43dc66673de6a1b2429db4e426ea9053222837c421ed3c017d51eeb146e335

Engine

Linux x86_64 33.7 MB Download → sha256 9207d83f802419fa21dcee80f4271b6285d96295bb5fb50b53e080969bdb887a
macOS Apple Silicon 27.4 MB Download → sha256 4e5ddea036a19b0347ffaf052e071a0aef5ad3aa771a7f8b8fe1b75f2a2f6d7d
macOS Intel 29.5 MB Download → sha256 ba58c8181d6126aa3ef19625d03cbcd210709a09cab6813eb60570c301b50ada
web 4.3 MB Download → sha256 f9ed0096fee4d3bd1fc3b6a3a8af90659e54f609131002c9bd916e2783cd5f75
Windows x86_64 33.4 MB Download → sha256 c4343f4151f00cdb00b64f5514a46bebf7895245b68511e1c11ec6eca6a25195

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.