pro_engineer — how it works

Pro Engineer/How it works

How it works — shared memory, one computed frame, and a bridge for Proton

The program computes. The panel draws.

Nothing here is required reading to use it. It is here because the shape of the thing explains most of its behaviour — why the in-game panel costs no frames, why a second simulator is an addition rather than a rewrite, and why a Linux install needs one extra process that Windows does not.

THE SHAPE

Three layers, and the arrows only go one way. A game is read, a frame is computed once, and every front end is handed the same frame.

# one folder per game, one Sink per place the frame goes,
# and any number of front ends

  Assetto Corsa ──▸ shared memory  ─┐
                                    ├─▸  the engineer  ──▸  one frame
  (your simulator) ────────────────┘                          │
                                                              ├─▸ shared memory ─▸ the CSP panel
                                                              ├─▸ UDP as JSON   ─▸ another machine
                                                              └─▸ the terminal

The split is not decoration. Whatever a front end needs has been worked out before it is asked to draw anything, which is what makes a Lua panel on a render thread affordable at all.

READING THE SIMULATOR

Assetto Corsa publishes physics, graphics and static session data in named shared memory. Reading it costs nothing and requires no hooks, no injected code and no modified game files — it is the interface the game offers for exactly this.

Read until two reads agreeA page caught mid-write is internally inconsistent. Each is re-read until two consecutive reads match; a frame that never settles is discarded rather than drawn. One bad frame in a thousand is a spike on a graph and a wrong verdict in an engineer.
Sixty times a secondFast enough that a lock-up lasting a fifth of a second is a dozen samples rather than one, and cheap enough to sit at about 0.1 % of one core.
Capabilities, stated honestlyEach game says what it can actually report. “Not measured” and “measured as zero” are different answers, and every wrong verdict this project has shipped came from confusing them — four tyres reading zero wear once became four destroyed tyres.

THE COMPUTED FRAME

Everything the front ends need, worked out once and written into one flat C-layout struct: speeds, pressures, temperatures, wear, fuel, the debrief, and up to eight lines of advice with their severity and confidence.

The telemetry screen in RG Pro Engineer showing speed, revs, pedals, grip and the four-corner tables
One frame, computed once, drawn by three different front ends.

Versioned, first field

The frame carries its layout version before anything else, so a reader checks it before trusting a single offset. A panel and an application that disagree say so instead of drawing eight bytes of one field as another.

The Lua view is generated

The panel's copy of the layout is emitted from the struct itself rather than written twice, and a test fails if the two ever disagree about the order of the fields — which size and count matching would not catch.

Eight advice slots

Fixed, named, and counted from the list rather than from a literal, so growing it is a line per slot in three places and a test that says which one was forgotten.

It carries the app's version

So the panel can notice that the game loaded an older copy of the application than the one now on disk — a case invisible from every other angle, because the files are current and the panel keeps drawing.

WHERE THE FRAME GOES

A sink is a place the computed frame is delivered. Shared memory for the in-game panel, UDP as JSON for anything else — a second front end, a friend watching from another machine, a relay for a championship.

The tick never waitsPublishing hands the frame over and returns. A sink may be slow, may fail, may vanish, and the loop that reads the game must not notice.
A failing sink is droppedAfter sixty consecutive failures — one second at tick rate — rather than logged sixty times a second for the rest of the session.
Remote sinks run slower on purposeA spectator cannot tell above about ten frames a second, and twenty cars at tick rate is megabytes a second arriving at a relay.
The UDP feed is plain JSONDocumented by its own shape. Writing your own front end against it is about fifteen lines of Python.
Another copy of this program gets the whole reading insteadSince v0.4.5 there are two things on the wire and neither replaces the other. A frame is a panel's worth of finished numbers; a reading is everything one tick of the simulator said, so the machine receiving it runs its own analysis. The difference is not size — there are no coordinates in a frame, so no map; no throttle, so no traces; no capabilities, so an unmeasured zero and a measured one read alike.

Nobody types an address unless they want to. Copies announce themselves on a multicast group and appear in each other's list — a couple of hundred bytes every two seconds, carrying a name, a role, a port, the car and the track, and nothing about the driving. Multicast to find, unicast to stream: a group is broadcast to a switch, and thirty readings a second on one is everybody's problem. It is all off unless you switch it on, because it is telemetry about a person.

WATCHING A FRIEND IN ANOTHER CITY

On one network there is nothing to set up: open LAN, one of you presses S and the other W, and you pick each other out of a list. Across the internet, two houses behind two routers is NAT and neither machine can dial the other — so the usual answer is a forwarded port, or a relay in the middle that somebody has to run.

There is a third answer, and it needs no code and no server. A mesh VPN — Tailscale or ZeroTier, both free for a handful of machines — gives each computer an address on one private network wherever it happens to be. Four steps, and nothing about the program changes:

1 · Both machines on one meshInstall it on each and sign both into the same network. Tailscale calls it a tailnet; ZeroTier calls it a network you join by its id.
2 · Read the address it gives eachTailscale's are 100.x.y.z; ZeroTier's are on the range you chose.
3 · The watcher presses WAnd leaves the listening address as 0.0.0.0:9001 — every interface this machine has, which includes the mesh one. Nothing else to set.
4 · The driver presses SAnd types the watcher's mesh address into sending to. It will not appear in the list, and that is expected — see below.

The list stays empty across a mesh, and that is not a fault. Finding each other is multicast, and a mesh VPN does not carry it — neither does most guest Wi-Fi. Typing the address is exactly the case that box has always existed for, and everything after it works identically. Announcing is only how you are found; it has nothing to do with how a session travels.

What each of you sets

On one network, nothing: open LAN, press S to share or W to watch, and pick each other out of the list. Across a mesh VPN there is no multicast, so the list stays empty and the address is typed — which is what that box has always been for.

# the driver, in config.json — send to your friend's mesh address
"overlay": { "broadcast_to": "100.64.0.2:9001" }

# the friend watching — listen on the mesh interface
"overlay": { "receive_from": "0.0.0.0:9001" }

Their screens then draw your session — the dashboard, the traces, the map, the corner table and the engineer. Not a copy of your sentences for them to read: the whole reading travels, so their machine finds the laps, builds the traces, draws the map's line and runs its own engineer over your driving.

What travels, and what does not

Everything one tick saidSpeed, pedals, steering, four wheels of temperature, pressure, wear, load and slip, the session, the car and the track — the same struct a game's reader produces, about two kilobytes, thirty times a second.
The analysis is theirs, not yoursWhich is what makes every screen work rather than one, and what puts the advice in their units and their language. Before v0.4.5 a spectator got eight finished sentences written on somebody else's machine.
Which measurements the game publishedCarried with the numbers, so a field their game does not measure is withheld on their screen too rather than drawn as a zero.
Not your recordsA watcher never writes somebody else's lap into their own personal bests. It is the one thing that would outlive the session, and it is the same rule the demo obeys.
Not anything about you, on the groupAn announcement carries a name you chose, whether you are driving or watching, a port, the car and the track. A test keeps it that way: a machine with no business here can read every packet and learn a name.

Whether it actually arrives

UDP has no retries, so the question that decides whether this is pleasant or flaky is how big each message is. Over about 1472 bytes and IP splits it into fragments; lose any one of them and the whole message is discarded. A tunnel has less room still — Tailscale and ZeroTier both default to 1280.

A whole reading, which is what another copy of this program gets1917 bytes — two fragments. Measured, and a test holds it there. Larger than one Ethernet frame, so IP splits it and losing either half discards the whole reading. At thirty a second that is about half a megabit per second, one way, per watcher — less than a video call, and far less than streaming a picture of your screen.
The summary frame, which is what a panel or a relay getsAbout 1200 bytes — one packet, on Ethernet and through a tunnel. A test pins that too, because a field added carelessly would push it over and the symptom would be a stuttering spectator blamed on the network.
On a local networkLoss on a switched LAN is close enough to zero that both are simply reliable. Over a mesh VPN the fragmenting is worth knowing about, which is why the rate is a setting and why the screen counts the readings that never came.

Checking it without a friend

Two probes ship with the source. The first says which of the four halves of the network is at fault — this machine's addresses, the port a session arrives on, the group copies find each other on, and whether anything is actually arriving:

# on the machine that will watch
cargo run -p ac_core --example lan_probe

# on the other one, aimed back at it
cargo run -p ac_core --example lan_probe -- send 192.168.1.42:9001

The second runs the real engineer, sends the finished frame through the real socket and prints what came out the far end — the older, summary path, which is still what a panel or a relay reads:

# both ends on this machine — proves everything but the network
cargo run -p ac_core --example share_probe

# or aim it at the far end's mesh address, and run it there with
# no argument to watch it arrive
cargo run -p ac_core --example share_probe -- 100.64.0.2:9001
share_probe — loopback
what the engineer said, before it went anywhere: All four brakes cooking: 910°C All four OVERHEATING: 119°C All four WORN OUT: 79.0% All four pressure: 31.0 psi (target 27.5 psi) on the wire: 1185 bytes — fits in one Ethernet frame, fits in one mesh-VPN frame what came back out the far end: All four brakes cooking: 910°C …and the other three 4 of 4 lines survived the trip

Where this stands, in full

What a complete online telemetry broadcaster would be, and which parts of it exist today. Everything marked done has been run, not reasoned about — the caveats are as interesting as the ticks, so both are here.

11WORKS
3WITH A CAVEAT
6NOT BUILT

works, and has been run ~ works, with something worth knowing · not built, and why

A mesh VPN is somebody else's service, and that is the trade. Nothing about your telemetry is stored by it — it carries packets between two machines you control — but it is an account with a company, which is a different answer from the rest of this program. On one network you need none of it.

WHY THE PANEL IS FREE

The in-game panel is a Custom Shaders Patch Lua app, and Lua runs on Assetto Corsa's render thread. At 165 Hz a millisecond is a sixth of the frame budget, and LuaJIT collects garbage mid-frame — so a panel that parsed text or built tables every frame would show up as a stutter rather than as a lower average frame rate.

It computes nothingEvery value it draws was calculated on the desktop side. The panel reads fields out of a struct and hands them to the UI.
Nothing is allocated per frameThat can be allocated once. Colours come from preallocated constants rather than being built inside the draw path.
Text is formatted on arrivalOnce per settled frame, not once per draw — and only the fields actually on screen are copied.
Drawn at any sizeThe platform offers five fixed font tiers and none of them is readable at 4K, so the panel draws text at a chosen size instead.

THE BRIDGE, ON LINUX ONLY

On Windows the application creates the shared mapping itself and there is nothing else to run. On Linux the game runs under Proton, so its shared memory lives inside the Wine prefix — and only a Windows process in there can create a mapping under the name the game is allowed to open.

That is the whole job of shm-bridge.exe: it runs inside the prefix and passes the mapping out to the native Linux side. It began as Damir Jelić's work and is used and extended here under its own MIT licence.

It can be identified while stoppedThe version is compiled into the binary, so a bridge sitting on disk and not running can still be asked what it is — there is no running a Windows binary from Linux to ask it.
It says what it mappedA note naming its version, the protocol, how many bytes it mapped and under what name, removed on a clean exit.
A mismatch is reported rather than silentA bridge older than the frame maps too few bytes and CSP quietly refuses to open the mapping — no error anywhere. The application checks instead, and the panel has one screen that names which of the three pieces does not fit.
Fetching one refuses a downgradeThe updater will not install a bridge that predates overlay support, because that bridge would map the game's own pages and never create the overlay mapping at all.

ADDING A SIMULATOR

A game is a folder under core/src/games/ implementing one small interface. It is not conditionals through the middle of the engineer, and that is deliberate: the engineer's rules should never have to know which simulator produced a number.

A source of framesWhatever the game publishes — shared memory, a UDP feed, a log — read into the same shape the rest of the program works in.
An honest capability listWhat it can actually report. This is the part that matters most: a game that cannot measure tyre wear must say so, not report zeros, or the engineer will announce four destroyed tyres on lap one.
Its own pathsWhere the game lives, where its setups are, where a front end would install itself.
Nothing elseThe engineer, the corner analysis, the confidence model, the debrief, the terminal and the frame are all shared, and none of them changes.

One implementation is not an abstraction, it is a guess. The second game is deliberately not started until the first is finished — the guesses are far cheaper to correct while there is one consumer of them. If you want a particular simulator, say so on the issues page; what it publishes decides how much of the above is even possible.

THE LICENCE

AGPL v3. Use it, race with it, read it, fork it. Change it for yourself and nothing at all is asked of you — the licence conditions passing a copy on, not what runs on your own machine. Publish something built on it and the one condition is that your source is open too, with this project credited.

Keeping your own source closed, or selling a product with this code inside it, needs written permission first: rgoshbbb@gmail.com. LICENSING.md is the plain-language version, including what counts as using the code — a translation into another language, an AI's included, is a derivative work; ideas and algorithms are not.