← All posts

The same bug on two platforms is not a platform bug

The report was a tiny window and no movement. The same on a laptop and on a handheld: two very different platforms, identical symptoms.

I spent hours on platform differences anyway.

That is the actual lesson here, so it goes first: identical behaviour on dissimilar platforms is evidence that the cause is not platform-specific. It is one of the more useful signals available in a bug report and I treated it as background noise.

The error was in the log

The cursor grab ran once, at startup, on the first frame, before the window manager had mapped and focused the window. Locking the cursor to an unfocused window fails, and the windowing library said so, in an error the game carried on past:

Unable to grab cursor: Cursor could not be confined

That line was in the logs from the beginning. I read past it repeatedly.

With the grab lost, the window never takes focus. Which produces the specific, confusing symptom pair: mouse look worked and the keyboard did not. Raw mouse motion reaches the application regardless of focus. Key events go to whatever the window manager believes is focused, which was not the game.

Two input methods, one working and one not, is the kind of detail that feels like it should narrow things down and instead invites theories about input backends. It had one cause, and the cause was not about input at all.

The grab now retries every frame until the window reports focus, then stops.

Waiting for a reply from itself

The second cause was independent and equally daft.

Offline practice seated the local player and then sent only world snapshots, never the message that says which player you are. A client cannot use a snapshot before it knows which slot is its own, so practice mode waited forever for an answer.

It reported an incomplete handshake while running inside the very process that had already seated it.

Every network test passed throughout, because the real network path sends that message correctly. The bug lived only in the offline shortcut, which no test covered because it looked too simple to fail.

The window

It had no explicit size and took the engine's small default. A first-person game in a letterbox cannot be evaluated. You cannot tell whether aiming feels right through a viewport that size. Now 1280x800, the handheld's native resolution.

Trivial, and it was half the report. "Tiny window" was not a cosmetic aside; it was the reason the rest of the report was hard to describe.

The thing I had claimed was the fix

Two days earlier I added a client prediction lead and presented it as the answer to this report. It was not.

That change stands on its own, and a client running level with the server does have its input discarded, and the lead is correct. But it did not fix this, and I said it did.

The Linux test that "proved" it passed for a reason that is almost funny: the test harness uses a tool that focuses the window before sending keys. That is precisely the step that was broken. The harness was performing the missing fix on the application's behalf, then reporting success.

A test that silently supplies the thing under test is worse than no test. It does not just fail to catch the bug. It actively certifies that the bug is gone.

What went in alongside

With movement working, the player got arms, a rifle, iron sights, a muzzle flash and a firing sound. The dummies got bodies with visible armour, and stopped spawning facing away from you.

One decision in that is worth recording. Aiming is deliberately not on the wire. There is no spread and no recoil yet, so every shot is a perfect ray down the view vector and aiming cannot change where a round lands. Putting an aim bit into the protocol today would mean a value every client must agree on and the server must validate, that changes nothing about the world, and that forces the determinism hash to be regenerated for a no-op.

So aiming is camera state: field of view narrows, the weapon centres, and sensitivity scales by the same ratio. That last part matters, because magnifying the view without scaling the control makes aiming feel worse than not aiming.

The trigger for moving it onto the wire is written down next to the code: the moment aiming affects the shot.