Skip to main content

Controller Polling and Memory Cards on PS1

Creator of Polygon Engine

Controller input and save data look like small gameplay services in an editor. On the original PlayStation they are external devices with protocols, connection states, and failure cases.

A useful engine API hides the transaction details without hiding the facts that matter to the game.

Buttons Arrive as Device State

The digital pad reports a bit field in which a cleared bit means the button is pressed. The runtime converts that active-low hardware representation into the positive questions gameplay code expects:

Input.GetButton(Input.CROSS)
Input.GetButtonDown(Input.CROSS)
Input.GetButtonUp(Input.CROSS)

isHeld comes from the current sample. Pressed and released states require both the current and previous samples:

pressed  = current down and previous up
released = current up and previous down

This edge detection belongs in the engine. Reimplementing it in every script creates inconsistent behavior at frame boundaries.

Not Every Pad Has the Same Capabilities

The PlayStation family includes digital pads, Analog Joystick/Dual Analog modes, and DualShock-style analog mode. A game cannot assume that two sticks are available because a controller is connected.

Polygon Engine exposes capability checks:

  • Input.IsConnected()
  • Input.HasAnalog()
  • Input.GetPadType()
  • Input.GetAxis(stickOrAction), returning normalized X and Y in -1..1
  • Input.L3 and Input.R3 when the pad exposes stick-click buttons

The hardware axis bytes use 0-255, centered near 128; the public Lua API returns normalized numbers, centered at 0. For a digital pad, the engine maps the D-pad onto the left-stick result so movement code can share one path. The right stick remains centered. A dual-stick camera checks Input.HasAnalog() and asks the player to enable analog mode rather than silently reassigning shoulder or trigger buttons.

Editor Play Mode feeds keyboard, mouse, SDL controllers, and generic joysticks into the public Input API. DuckStation still needs the correct emulated device selected: Digital Controller for digital-only projects or Analog Controller/DualShock with analog mode enabled for dual-stick and L3/R3 input.

The editor is an iteration environment, not proof of a particular controller, emulator mapping, or console revision. Validate the packaged build on each intended target configuration.

A Memory Card Save Is a File Format

Memory-card persistence needs more than writing gameplay bytes. A standard save file includes platform-facing metadata and an engine payload.

Polygon Engine writes one 8,192-byte block containing:

standard memory-card header
-> display title
-> 16-color icon palette and icon data
versioned Polygon Engine payload
-> key/value entries
-> checksum

The project defines a unique save-file ID and display title. This prevents two Polygon Engine games from silently targeting the same card file and gives the save a recognizable name in a memory-card browser.

The runtime supports 32 entries. Keys are limited to 31 characters plus a terminator; values are limited to 63 characters plus a terminator. The fixed layout keeps memory ownership and file size predictable.

Writes Can Fail

Gameplay code must treat saving as an operation with a result:

if not SaveData.Write("checkpoint", "hub") then
Log.Print(SaveData.GetLastError())
return
end

if not SaveData.Flush() then
Log.Print(SaveData.GetLastError())
end

SaveData.Write() changes the in-memory document. SaveData.Flush() persists the complete document by writing and verifying the inactive journal generation inside the existing block.

Useful failure states include:

  • no card or no save file,
  • invalid or oversized key/value,
  • entry capacity reached,
  • file creation failure,
  • incomplete write,
  • corrupt checksum or payload,
  • unsupported save version.

The game should surface those outcomes. A silent save icon is not enough if the card was removed or the write failed.

Versioning and Migration

The current format is versioned and checksummed. On load, the runtime distinguishes an empty card from corrupted data and from a newer unsupported version.

Polygon Engine can also read its supported older payload. The data is kept in memory and rewritten in the standard format after the next successful flush. Migration happens at the format boundary rather than inside gameplay scripts.

This is a small system, but it has the properties a shipping save path needs:

  • a platform-recognizable header,
  • per-project identity,
  • bounded payload,
  • corruption detection,
  • explicit error reporting,
  • backward migration for the supported older format.

The Engine Lesson

Input and saving are often presented as single function calls. The hardware view is more useful:

controller: poll -> identify -> normalize -> derive transitions
memory card: encode -> create/write -> verify status -> report to gameplay

Polygon Engine keeps those device concerns in the runtime and exposes a small Lua surface. Scripts receive stable game concepts while retaining enough status information to handle the real device correctly.

Continue Reading

Test the player experience

Use View > Save Data Simulator to test missing, full, and unreadable saves while developing. Check the return value of each save or load operation, show a clear message, and provide a retry option. Repeat the flow in DuckStation and on your target console with a test Memory Card.

See Persistence for examples and the key/value limits.

Updated for Polygon Engine 0.1.0-ea.2.