<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://polygonengine.novemtails.com/blog</id>
    <title>Polygon Engine Blog</title>
    <updated>2026-05-14T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://polygonengine.novemtails.com/blog"/>
    <subtitle>Polygon Engine Blog</subtitle>
    <icon>https://polygonengine.novemtails.com/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[What It Takes to Ship a PS1 Project from an Editor]]></title>
        <id>https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package</id>
        <link href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package"/>
        <updated>2026-05-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The goal of Polygon Engine is not only to render a PS1 scene. The goal is for a user to create a game from the editor, validate it, build it, test it, and understand why something fails when it fails.]]></summary>
        <content type="html"><![CDATA[<p>The goal of Polygon Engine is not only to render a PS1 scene. The goal is for a user to create a game from the editor, validate it, build it, test it, and understand why something fails when it fails.</p>
<p>That is a much harder target than a one-off demo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-minimum-product-bar">The Minimum Product Bar<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#the-minimum-product-bar" class="hash-link" aria-label="Direct link to The Minimum Product Bar" title="Direct link to The Minimum Product Bar" translate="no">​</a></h2>
<p>A shippable PS1 editor workflow needs all of these pieces to cooperate:</p>
<ul>
<li class="">project creation and loading,</li>
<li class="">asset import,</li>
<li class="">stable GUID references,</li>
<li class="">scene editing,</li>
<li class="">component authoring,</li>
<li class="">Level Builder mesh creation,</li>
<li class="">Lua gameplay scripts,</li>
<li class="">UI canvas authoring,</li>
<li class="">build settings,</li>
<li class="">validation,</li>
<li class="">runtime selection (or compilation in a private source build),</li>
<li class="">asset staging,</li>
<li class="">disc packaging,</li>
<li class="">emulator testing,</li>
<li class="">hardware-oriented release guidance.</li>
</ul>
<p>If any one of those is vague, the user ends up debugging engine internals instead of making a game.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-output-has-to-be-inspectable">Build Output Has to Be Inspectable<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#build-output-has-to-be-inspectable" class="hash-link" aria-label="Direct link to Build Output Has to Be Inspectable" title="Direct link to Build Output Has to Be Inspectable" translate="no">​</a></h2>
<p>Polygon Engine separates staged disc data from final package output:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Build/ps1/disc_root/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  SYSTEM.CNF</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  POLYGON.EXE</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  DATA/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    MANIFEST.JSN</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    S/  scenes</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    L/  Lua scripts</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    M/  meshes</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    R/  materials</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    T/  textures</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    A/  resident audio</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    X/  XA music</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Build/ps1/package/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  *.bin</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  *.cue</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  *.iso</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  polygon.xml</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  README_BURN_PS1.txt</span><br></span></code></pre></div></div>
<p>This makes build failures easier to reason about. If the runtime says it cannot load a script, the staged <code>DATA/L</code> folder and manifest are visible. If packaging fails, the package folder and mkpsxiso log are visible.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="failure-modes-the-editor-prevents">Failure Modes the Editor Prevents<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#failure-modes-the-editor-prevents" class="hash-link" aria-label="Direct link to Failure Modes the Editor Prevents" title="Direct link to Failure Modes the Editor Prevents" translate="no">​</a></h2>
<p>The engine has already run into the kinds of problems a real user would hit:</p>
<ul>
<li class="">missing binary assets,</li>
<li class="">scenes not included in the build,</li>
<li class="">script load errors,</li>
<li class="">runtime out-of-memory errors,</li>
<li class="">input mapping confusion,</li>
<li class="">colliders missing from visible meshes,</li>
<li class="">near-plane face dropout,</li>
<li class="">polygon overlap from large surfaces,</li>
<li class="">performance drops from too much scene work,</li>
<li class="">SPU/VRAM pressure from assets.</li>
</ul>
<p>Those failures become validation, documentation, and safer defaults instead of project-specific knowledge.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="validation-as-a-product-feature">Validation as a Product Feature<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#validation-as-a-product-feature" class="hash-link" aria-label="Direct link to Validation as a Product Feature" title="Direct link to Validation as a Product Feature" translate="no">​</a></h2>
<p>The Problems panel is one of the most important parts of the editor. It turns runtime pain into editor feedback.</p>
<p>Good validation says:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">This Level Builder mesh has no Collider and can be walked through.</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">This startup scene is not included in Scenes In Build.</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">This Lua heap exceeds the PS1 runtime cap.</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">This material is transparent and needs careful PS1 ordering.</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">This primitive cell size is too coarse for stable PS1 depth ordering.</span><br></span></code></pre></div></div>
<p>That is more valuable than a generic build failure. The user can act on it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="emulator-testing-is-necessary-but-not-sufficient">Emulator Testing Is Necessary but Not Sufficient<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#emulator-testing-is-necessary-but-not-sufficient" class="hash-link" aria-label="Direct link to Emulator Testing Is Necessary but Not Sufficient" title="Direct link to Emulator Testing Is Necessary but Not Sufficient" translate="no">​</a></h2>
<p>DuckStation is the right fast loop for day-to-day work. It catches:</p>
<ul>
<li class="">boot errors,</li>
<li class="">missing files,</li>
<li class="">script errors,</li>
<li class="">input problems,</li>
<li class="">performance regressions,</li>
<li class="">obvious rendering artifacts.</li>
</ul>
<p>But emulator success is not the same thing as a hardware claim. Real hardware remains the final confidence gate, especially for timing, disc behavior, audio, and edge-case rendering.</p>
<p>DuckStation is the fast test loop; real hardware is the release-quality check.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="supported-boundaries">Supported Boundaries<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#supported-boundaries" class="hash-link" aria-label="Direct link to Supported Boundaries" title="Direct link to Supported Boundaries" translate="no">​</a></h2>
<p>Polygon Engine keeps the supported path explicit. It does not claim to provide:</p>
<ul>
<li class="">arbitrary mesh boolean optimization,</li>
<li class="">general-purpose world streaming,</li>
<li class="">unlimited Lua,</li>
<li class="">arbitrary simultaneous audio streaming beyond the single CD-XA music path,</li>
<li class="">modern lighting,</li>
<li class="">order-independent transparency,</li>
<li class="">automatic perfect conversion for every primitive,</li>
<li class="">hardware certification without real hardware testing.</li>
</ul>
<p>Clear limits make the supported path stronger.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="production-checklist">Production Checklist<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#production-checklist" class="hash-link" aria-label="Direct link to Production Checklist" title="Direct link to Production Checklist" translate="no">​</a></h2>
<p>Before calling a project release-ready, the workflow should pass this checklist:</p>
<table><thead><tr><th>Area</th><th>Requirement</th></tr></thead><tbody><tr><td>Scenes</td><td>Startup scene and all loadable scenes included in build</td></tr><tr><td>Assets</td><td>No missing mesh, material, texture, audio, or script references</td></tr><tr><td>Geometry</td><td>Large floors/walls chunked or baked into safer pieces</td></tr><tr><td>Collision</td><td>Blocking objects have Colliders; triggers are intentional</td></tr><tr><td>Lua</td><td>Script counts, heap, properties, and instruction budget validated</td></tr><tr><td>UI</td><td>Authored UI stays within element budget</td></tr><tr><td>Audio</td><td>Resident clips fit SPU RAM; XA tracks stay within the streaming limit</td></tr><tr><td>Save data</td><td>Card errors are surfaced and each game uses a unique save-file ID</td></tr><tr><td>Performance</td><td>Scene runs acceptably in emulator with debug overlays off</td></tr><tr><td>Package</td><td><code>.cue</code> and <code>.bin</code> generated and inspected</td></tr><tr><td>Hardware</td><td>Final build tested on the real target before public claims</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-the-complete-path-matters">Why the Complete Path Matters<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#why-the-complete-path-matters" class="hash-link" aria-label="Direct link to Why the Complete Path Matters" title="Direct link to Why the Complete Path Matters" translate="no">​</a></h2>
<p>PS1 homebrew tooling is still a small space. A visual editor, Lua scripting, binary asset pipeline, and PSn00bSDK runtime in one workflow is unusual enough that the technical details matter.</p>
<p>The editor does not replace the target architecture. It moves expensive conversion and descriptive validation to Windows, then sends compact, bounded data to the console.</p>
<p>That end-to-end contract is what turns an editor demonstration into a repeatable PS1 production workflow.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation">A Practical Map of the Original PlayStation</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio">The CD-ROM Is Not Extra RAM</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards">Controller Polling and Memory Cards on PS1</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/docs/build/pipeline">Build pipeline reference</a></li>
</ul>
<p><em>Updated for Polygon Engine 0.1.0-ea.2.</em></p>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="PS1" term="PS1"/>
        <category label="Build" term="Build"/>
        <category label="Packaging" term="Packaging"/>
        <category label="Workflow" term="Workflow"/>
        <category label="Debugging" term="Debugging"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Controller Polling and Memory Cards on PS1]]></title>
        <id>https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards</id>
        <link href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards"/>
        <updated>2026-04-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[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.]]></summary>
        <content type="html"><![CDATA[<p>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.</p>
<p>A useful engine API hides the transaction details without hiding the facts that matter to the game.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="buttons-arrive-as-device-state">Buttons Arrive as Device State<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#buttons-arrive-as-device-state" class="hash-link" aria-label="Direct link to Buttons Arrive as Device State" title="Direct link to Buttons Arrive as Device State" translate="no">​</a></h2>
<p>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:</p>
<div class="language-lua codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-lua codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetButton</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CROSS</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetButtonDown</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CROSS</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetButtonUp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CROSS</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></span></code></pre></div></div>
<p><code>isHeld</code> comes from the current sample. Pressed and released states require both the current and previous samples:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">pressed  = current down and previous up</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">released = current up and previous down</span><br></span></code></pre></div></div>
<p>This edge detection belongs in the engine. Reimplementing it in every script creates inconsistent behavior at frame boundaries.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="not-every-pad-has-the-same-capabilities">Not Every Pad Has the Same Capabilities<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#not-every-pad-has-the-same-capabilities" class="hash-link" aria-label="Direct link to Not Every Pad Has the Same Capabilities" title="Direct link to Not Every Pad Has the Same Capabilities" translate="no">​</a></h2>
<p>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.</p>
<p>Polygon Engine exposes capability checks:</p>
<ul>
<li class=""><code>Input.IsConnected()</code></li>
<li class=""><code>Input.HasAnalog()</code></li>
<li class=""><code>Input.GetPadType()</code></li>
<li class=""><code>Input.GetAxis(stickOrAction)</code>, returning normalized X and Y in <code>-1..1</code></li>
<li class=""><code>Input.L3</code> and <code>Input.R3</code> when the pad exposes stick-click buttons</li>
</ul>
<p>The hardware axis bytes use <code>0-255</code>, centered near <code>128</code>; the public Lua API returns normalized numbers, centered at <code>0</code>. 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 <code>Input.HasAnalog()</code> and asks the player to enable analog mode rather than silently reassigning shoulder or trigger buttons.</p>
<p>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.</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-memory-card-save-is-a-file-format">A Memory Card Save Is a File Format<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#a-memory-card-save-is-a-file-format" class="hash-link" aria-label="Direct link to A Memory Card Save Is a File Format" title="Direct link to A Memory Card Save Is a File Format" translate="no">​</a></h2>
<p>Memory-card persistence needs more than writing gameplay bytes. A standard save file includes platform-facing metadata and an engine payload.</p>
<p>Polygon Engine writes one 8,192-byte block containing:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">standard memory-card header</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; display title</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; 16-color icon palette and icon data</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">versioned Polygon Engine payload</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; key/value entries</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; checksum</span><br></span></code></pre></div></div>
<p>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.</p>
<p>The runtime supports <code>32</code> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="writes-can-fail">Writes Can Fail<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#writes-can-fail" class="hash-link" aria-label="Direct link to Writes Can Fail" title="Direct link to Writes Can Fail" translate="no">​</a></h2>
<p>Gameplay code must treat saving as an operation with a result:</p>
<div class="language-lua codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-lua codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">not</span><span class="token plain"> SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Write</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"checkpoint"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"hub"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    Log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetLastError</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">not</span><span class="token plain"> SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Flush</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    Log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetLastError</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><br></span></code></pre></div></div>
<p><code>SaveData.Write()</code> changes the in-memory document. <code>SaveData.Flush()</code> persists the complete document by writing and verifying the inactive journal generation inside the existing block.</p>
<p>Useful failure states include:</p>
<ul>
<li class="">no card or no save file,</li>
<li class="">invalid or oversized key/value,</li>
<li class="">entry capacity reached,</li>
<li class="">file creation failure,</li>
<li class="">incomplete write,</li>
<li class="">corrupt checksum or payload,</li>
<li class="">unsupported save version.</li>
</ul>
<p>The game should surface those outcomes. A silent save icon is not enough if the card was removed or the write failed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="versioning-and-migration">Versioning and Migration<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#versioning-and-migration" class="hash-link" aria-label="Direct link to Versioning and Migration" title="Direct link to Versioning and Migration" translate="no">​</a></h2>
<p>The current format is versioned and checksummed. On load, the runtime distinguishes an empty card from corrupted data and from a newer unsupported version.</p>
<p>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.</p>
<p>This is a small system, but it has the properties a shipping save path needs:</p>
<ul>
<li class="">a platform-recognizable header,</li>
<li class="">per-project identity,</li>
<li class="">bounded payload,</li>
<li class="">corruption detection,</li>
<li class="">explicit error reporting,</li>
<li class="">backward migration for the supported older format.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-engine-lesson">The Engine Lesson<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#the-engine-lesson" class="hash-link" aria-label="Direct link to The Engine Lesson" title="Direct link to The Engine Lesson" translate="no">​</a></h2>
<p>Input and saving are often presented as single function calls. The hardware view is more useful:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">controller: poll -&gt; identify -&gt; normalize -&gt; derive transitions</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">memory card: encode -&gt; create/write -&gt; verify status -&gt; report to gameplay</span><br></span></code></pre></div></div>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui">Lua Gameplay on a Fixed-Memory PS1 Runtime</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package">What It Takes to Ship a PS1 Project from an Editor</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/docs/scripting/input">Input reference</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/docs/scripting/savedata">SaveData reference</a></li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="test-the-player-experience">Test the player experience<a href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards#test-the-player-experience" class="hash-link" aria-label="Direct link to Test the player experience" title="Direct link to Test the player experience" translate="no">​</a></h2>
<p>Use <strong>View &gt; Save Data Simulator</strong> 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.</p>
<p>See <a class="" href="https://polygonengine.novemtails.com/docs/scripting/savedata">Persistence</a> for examples and the key/value limits.</p>
<p><em>Updated for Polygon Engine 0.1.0-ea.2.</em></p>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="PS1" term="PS1"/>
        <category label="Hardware" term="Hardware"/>
        <category label="Input" term="Input"/>
        <category label="Memory Card" term="Memory Card"/>
        <category label="Save Data" term="Save Data"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Assets, Audio, Lighting, and the Budgets Behind Them]]></title>
        <id>https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets</id>
        <link href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets"/>
        <updated>2026-04-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The fastest way to make a PS1 demo feel real is to add textures, sound, lighting, UI, and animation. The fastest way to make it collapse is to add those systems without budgets.]]></summary>
        <content type="html"><![CDATA[<p>The fastest way to make a PS1 demo feel real is to add textures, sound, lighting, UI, and animation. The fastest way to make it collapse is to add those systems without budgets.</p>
<p>Polygon Engine treats asset import and runtime budgets as the same problem. If something can be authored in the editor, it has to be countable before the build.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-memory-picture">The Memory Picture<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#the-memory-picture" class="hash-link" aria-label="Direct link to The Memory Picture" title="Direct link to The Memory Picture" translate="no">​</a></h2>
<p>The original PlayStation gives the engine very little room:</p>
<table><thead><tr><th>Resource</th><th>Size</th><th>What competes for it</th></tr></thead><tbody><tr><td>Main RAM</td><td><code>2 MB</code></td><td>Runtime, scene data, meshes, Lua, buffers, game state</td></tr><tr><td>VRAM</td><td><code>1 MB</code></td><td>Framebuffers, textures, CLUTs</td></tr><tr><td>SPU RAM</td><td><code>512 KB</code></td><td>Audio sample data</td></tr><tr><td>CPU</td><td><code>33 MHz R3000A</code></td><td>Gameplay, transforms, collision, scripting, build-time assumptions made visible</td></tr><tr><td>GTE</td><td>Fixed-point coprocessor</td><td>Transform, lighting, projection</td></tr></tbody></table>
<p>This is why the editor has to show budgets. A project can be visually tiny and still fail if it uses too many textures, clips, scripts, or UI elements.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="textures">Textures<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#textures" class="hash-link" aria-label="Direct link to Textures" title="Direct link to Textures" translate="no">​</a></h2>
<p>Textures are imported into PS1-oriented <code>.tex</code> data. The runtime validates the texture header, data size, format, dimensions, and VRAM placement before uploading.</p>
<p>The current runtime limits include:</p>
<ul>
<li class=""><code>64</code> texture assets,</li>
<li class=""><code>20</code> texture page slots,</li>
<li class=""><code>1 MB</code> total VRAM shared with framebuffers.</li>
</ul>
<p>Texture import supports indexed and direct formats, but content still needs discipline. A single large texture can be more expensive than several small, reusable ones. UI images also use texture memory, so menus and HUD art must be budgeted with the world.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="materials">Materials<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#materials" class="hash-link" aria-label="Direct link to Materials" title="Direct link to Materials" translate="no">​</a></h2>
<p>Materials are intentionally simple:</p>
<ul>
<li class="">base color,</li>
<li class="">optional main texture,</li>
<li class="">render mode,</li>
<li class="">double-sided flag,</li>
<li class="">vertex color multiply,</li>
<li class="">dithering.</li>
</ul>
<p>That simplicity is part of the PS1 design. The runtime is not trying to emulate a modern material graph. It is trying to emit predictable GPU primitives.</p>
<p>The editor warns when a material choice is likely to be risky:</p>
<ul>
<li class="">double-sided materials increase draw cost and can make ordering less stable,</li>
<li class="">transparent materials need careful placement and should stay small or isolated,</li>
<li class="">missing texture assets should fail validation before runtime.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="audio">Audio<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#audio" class="hash-link" aria-label="Direct link to Audio" title="Direct link to Audio" translate="no">​</a></h2>
<p>Audio starts from imported WAV files, then follows one of two PS1 paths.</p>
<p>Resident clips are converted to VAG ADPCM, loaded into SPU RAM, and played
through Audio Source components or the <code>AudioSource</code> Lua API. Streaming music
is encoded as CD-XA data and played from the disc through the <code>Music</code> Lua API.</p>
<table><thead><tr><th>Path</th><th style="text-align:right">Current limit</th><th>Primary budget</th></tr></thead><tbody><tr><td>Resident VAG clips</td><td style="text-align:right"><code>16</code></td><td>512 KB SPU RAM, minus the reserved system area</td></tr><tr><td>Streaming XA tracks</td><td style="text-align:right"><code>8</code></td><td>CD bandwidth and seek coordination</td></tr></tbody></table>
<p>Short effects and latency-sensitive loops belong in the resident path. Long music belongs in XA streaming. Marking a long track as a resident Audio Source can still exhaust SPU RAM even when the source WAV looks modest on disk.</p>
<p>The import workflow is:</p>
<ol>
<li class="">import WAV,</li>
<li class="">choose resident VAG or Streaming Music (CD-XA),</li>
<li class="">choose a target sample rate for resident audio,</li>
<li class="">assign VAG clips to Audio Source or address XA tracks by name,</li>
<li class="">validate SPU use and streaming-track count,</li>
<li class="">trigger playback through Play On Start, <code>AudioSource</code>, or <code>Music</code>.</li>
</ol>
<p>The runtime suspends XA playback while it performs a scene load and restarts the suspended track from its beginning afterward. This keeps scene reads and music streaming from competing for incompatible CD access.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lighting">Lighting<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#lighting" class="hash-link" aria-label="Direct link to Lighting" title="Direct link to Lighting" translate="no">​</a></h2>
<p>Lighting is GTE-oriented and deliberately limited. The runtime supports a small number of directional lights plus ambient contribution. The current runtime cap is <code>3</code> scene directional lights.</p>
<p>That fits the hardware. Directional lighting gives low-poly scenes shape and readability without pretending the renderer has:</p>
<ul>
<li class="">per-pixel lights,</li>
<li class="">dynamic shadows,</li>
<li class="">normal maps,</li>
<li class="">physically based materials.</li>
</ul>
<p>Lua can still create gameplay moments by enabling/disabling Light GameObjects or sequencing scene states. The important part is that the lighting model stays understandable and cheap.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ui-and-particles">UI and Particles<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#ui-and-particles" class="hash-link" aria-label="Direct link to UI and Particles" title="Direct link to UI and Particles" translate="no">​</a></h2>
<p>Authored UI has a fixed scene element budget of <code>32</code>. Particles are also capped. These limits are not glamour features, but they keep frame time and memory predictable.</p>
<p>For UI, the production rule is:</p>
<ul>
<li class="">use authored UI for shipping menus and HUD,</li>
<li class="">keep element count small,</li>
<li class="">use <code>UI.DrawText()</code> for temporary/debug text only,</li>
<li class="">avoid texture-heavy UI unless it is part of the art budget.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-budget-dashboard">The Budget Dashboard<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#the-budget-dashboard" class="hash-link" aria-label="Direct link to The Budget Dashboard" title="Direct link to The Budget Dashboard" translate="no">​</a></h2>
<p>The PS1 Budget dashboard is a bridge between content and runtime. It summarizes:</p>
<ul>
<li class="">GameObject count,</li>
<li class="">UI count,</li>
<li class="">unique meshes,</li>
<li class="">materials,</li>
<li class="">textures,</li>
<li class="">texture page placement,</li>
<li class="">audio clips,</li>
<li class="">Lua heap,</li>
<li class="">packet buffer estimate,</li>
<li class="">scene triangles,</li>
<li class="">warnings and hard issues.</li>
</ul>
<p>This is essential for a user-facing engine. The user should not need to know C constants to understand that a scene is drifting out of bounds.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-makes-this-project-interesting">What Makes This Project Interesting<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#what-makes-this-project-interesting" class="hash-link" aria-label="Direct link to What Makes This Project Interesting" title="Direct link to What Makes This Project Interesting" translate="no">​</a></h2>
<p>Most modern engine workflows assume the target is forgiving. Polygon Engine is interesting because the target is not forgiving at all.</p>
<p>The asset pipeline is therefore not just "import files." It is a system that turns friendly editor content into something a 1994 console can actually load, sort, light, play, and keep in memory.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts">VRAM Is a Layout, Not a Texture Folder</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio">The CD-ROM Is Not Extra RAM</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui">Lua Gameplay on a Fixed-Memory PS1 Runtime</a></li>
</ul>
<p>The current RAM model includes actual scene-object storage and separate load
peaks for the old scene plus BSN read and the new scene plus asset/script
staging. Live diagnostics expose allocation requests and nested subsystem
times; they do not measure the largest free heap block. <code>PS1 Ready</code> is a static
budget result. See the <a class="" href="https://polygonengine.novemtails.com/docs/ps1/budget-dashboard">Budget Dashboard</a> before
using it as release evidence.</p>
<p><em>Updated for Polygon Engine 0.1.0-ea.2.</em></p>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="Audio" term="Audio"/>
        <category label="Lighting" term="Lighting"/>
        <category label="PS1" term="PS1"/>
        <category label="Runtime" term="Runtime"/>
        <category label="Optimization" term="Optimization"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Lua Gameplay on a Fixed-Memory PS1 Runtime]]></title>
        <id>https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui</id>
        <link href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui"/>
        <updated>2026-03-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Lua is valuable in a PS1 engine because it separates gameplay iteration from C runtime work. Lua is also dangerous on a PS1 if it grows without limits.]]></summary>
        <content type="html"><![CDATA[<p>Lua is valuable in a PS1 engine because it separates gameplay iteration from C runtime work. Lua is also dangerous on a PS1 if it grows without limits.</p>
<p>Polygon Engine's scripting model is built around that tension: scripts should be easy to attach, inspect, and test, while the runtime keeps memory, script count, and API surface bounded.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-lua-owns">What Lua Owns<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#what-lua-owns" class="hash-link" aria-label="Direct link to What Lua Owns" title="Direct link to What Lua Owns" translate="no">​</a></h2>
<p>In Polygon Engine, Lua is responsible for gameplay behavior:</p>
<ul>
<li class="">responding to triggers and collisions,</li>
<li class="">driving pickups, gates, terminals, and stations,</li>
<li class="">updating UI text and focus,</li>
<li class="">calling scene loads,</li>
<li class="">playing audio sources,</li>
<li class="">toggling GameObjects,</li>
<li class="">reading and writing small save data values.</li>
</ul>
<p>Lua does not own the renderer, asset loader, SPU upload path, mesh format, or build pipeline. Those stay in C/C++.</p>
<p>That split is deliberate. The PS1 runtime should expose a small set of stable APIs instead of letting scripts reach into every subsystem.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="gameobject-attached-scripts">GameObject-Attached Scripts<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#gameobject-attached-scripts" class="hash-link" aria-label="Direct link to GameObject-Attached Scripts" title="Direct link to GameObject-Attached Scripts" translate="no">​</a></h2>
<p>Scripts are attached to scene GameObjects through a Lua Behaviour component.
The runtime invokes lifecycle functions when they exist:</p>
<div class="language-lua codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-lua codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">Start</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">gameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    Log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">GameObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">gameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">..</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">" ready"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">Update</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">gameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> deltaTime</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    Transform</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">RotateY</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">gameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">45</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain"> deltaTime</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">OnTriggerEnter</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">gameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> otherGameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    Log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"triggered by "</span><span class="token plain"> </span><span class="token operator">..</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">tostring</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">otherGameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><br></span></code></pre></div></div>
<p>The engine also supports script-to-script style calls through
<code>GameObject.Invoke</code> and UI Button callback targets. Build/editor validation can
detect missing callback functions before the runtime is launched.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="runtime-scripting-limits">Runtime Scripting Limits<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#runtime-scripting-limits" class="hash-link" aria-label="Direct link to Runtime Scripting Limits" title="Direct link to Runtime Scripting Limits" translate="no">​</a></h2>
<p>The current scripting limits are intentionally visible:</p>
<table><thead><tr><th>Limit</th><th>Value</th></tr></thead><tbody><tr><td>Max script components</td><td><code>32</code></td></tr><tr><td>Max unique script sources</td><td><code>32</code></td></tr><tr><td>Max properties per script component</td><td><code>8</code></td></tr><tr><td>Max script property name</td><td><code>32</code> bytes</td></tr><tr><td>Max script property value</td><td><code>64</code> bytes</td></tr><tr><td>Max Lua heap</td><td><code>512 KB</code></td></tr><tr><td>Max individual script size</td><td><code>64 KB</code></td></tr></tbody></table>
<p>The default project can choose smaller settings, but it cannot exceed the runtime cap. The build validates those caps.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="script-properties">Script Properties<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#script-properties" class="hash-link" aria-label="Direct link to Script Properties" title="Direct link to Script Properties" translate="no">​</a></h2>
<p>Script properties are one of the most useful workflow features. They let the same Lua file behave differently per GameObject.</p>
<p>For example, a station script can expose:</p>
<div class="language-lua codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-lua codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">stationId </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">3</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">promptText </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Activate"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">targetGameObjectId </span><span class="token operator">=</span><span class="token plain"> </span><span class="token operator">-</span><span class="token number">1</span><br></span></code></pre></div></div>
<p>The editor reads defaults, lets the user override values in the Inspector, and stages those values with the scene. The runtime passes them to the script component.</p>
<p>This is the kind of feature that makes a PS1 editor practical: fewer duplicated scripts, more data-driven GameObject behavior.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="instruction-budget">Instruction Budget<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#instruction-budget" class="hash-link" aria-label="Direct link to Instruction Budget" title="Direct link to Instruction Budget" translate="no">​</a></h2>
<p>Runaway scripts are especially harmful on a fixed runtime. A bad loop can steal the entire frame and make the game look like a renderer problem.</p>
<p>Polygon Engine exposes an instruction budget setting. When enabled, Lua calls are guarded so runaway scripts can be stopped instead of silently destroying frame time.</p>
<p>The build also warns when the instruction hook interval is larger than the per-call budget, because that configuration lets scripts overshoot before being interrupted.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="authored-ui-not-just-debug-text">Authored UI, Not Just Debug Text<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#authored-ui-not-just-debug-text" class="hash-link" aria-label="Direct link to Authored UI, Not Just Debug Text" title="Direct link to Authored UI, Not Just Debug Text" translate="no">​</a></h2>
<p>The runtime supports up to eight named UI canvases with independent reference
sizes, visibility, and sort order. The fixed scene-wide element budget covers:</p>
<ul>
<li class="">Panel,</li>
<li class="">Text,</li>
<li class="">Image,</li>
<li class="">Button,</li>
<li class="">ProgressBar.</li>
</ul>
<p>Lua can find UI elements, change text, progress, position, size, and color,
toggle element or canvas visibility, change canvas order, and set or read focus:</p>
<div class="language-lua codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-lua codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">local</span><span class="token plain"> progressText </span><span class="token operator">=</span><span class="token plain"> </span><span class="token operator">-</span><span class="token number">1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">local</span><span class="token plain"> hudCanvas </span><span class="token operator">=</span><span class="token plain"> </span><span class="token operator">-</span><span class="token number">1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">Start</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">gameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    hudCanvas </span><span class="token operator">=</span><span class="token plain"> Canvas</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Find</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"HUD"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    Canvas</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">SetEnabled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">hudCanvas</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">true</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    progressText </span><span class="token operator">=</span><span class="token plain"> UI</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Find</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"Progress Text"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    Text</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">SetText</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">progressText</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Stations 0/5"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">Update</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">gameObjectId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> deltaTime</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetButtonDown</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">Input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CROSS</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        Text</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">SetText</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">progressText</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Activated"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><br></span></code></pre></div></div>
<p>This is different from drawing temporary debug text. The authored UI is part of the scene data and is staged for the runtime.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="save-data">Save Data<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#save-data" class="hash-link" aria-label="Direct link to Save Data" title="Direct link to Save Data" translate="no">​</a></h2>
<p>The SaveData API is a bounded key-value layer backed by one 8,192-byte memory-card block:</p>
<div class="language-lua codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-lua codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Write</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"checkpoint"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"hub"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">and</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">   SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Write</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"station_03_complete"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"1"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">not</span><span class="token plain"> SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Flush</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        Log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">GetLastError</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">end</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">local</span><span class="token plain"> checkpoint </span><span class="token operator">=</span><span class="token plain"> SaveData</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Read</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"checkpoint"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></span></code></pre></div></div>
<p>The runtime supports <code>32</code> entries, with 31-character keys and 63-character values. The file includes a standard memory-card header, project-defined title, 16-color icon, versioned payload, and checksum.</p>
<p>The API reports card absence, capacity errors, invalid values, incomplete writes, corrupt payloads, and unsupported versions. Gameplay code can therefore show a real save result instead of assuming that <code>flush()</code> succeeded.</p>
<p>Supported older save payloads are decoded and rewritten in the current format
after the next successful flush. Migration stays inside the persistence layer;
gameplay scripts continue to read the same keys.</p>
<p>The system is intentionally small. It covers checkpoints, settings, progress flags, and compact player state without turning the memory card into a general database.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-approach-fits-ps1">Why This Approach Fits PS1<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#why-this-approach-fits-ps1" class="hash-link" aria-label="Direct link to Why This Approach Fits PS1" title="Direct link to Why This Approach Fits PS1" translate="no">​</a></h2>
<p>The PS1 does not have room for an unbounded scripting environment. The engine has to make Lua feel friendly while keeping it measurable.</p>
<p>The rule is:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Lua owns game rules.</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">C runtime owns hardware.</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Editor owns validation.</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Build owns packaging.</span><br></span></code></pre></div></div>
<p>That separation is what makes Lua practical for PS1 homebrew instead of just impressive in a screenshot.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/controller-polling-and-memory-cards">Controller Polling and Memory Cards on PS1</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline">From Editor JSON to PS1 Runtime Data</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/docs/scripting/savedata">SaveData reference</a></li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="saving-and-testing-your-game">Saving and testing your game<a href="https://polygonengine.novemtails.com/blog/lua-gameplay-save-data-and-ui#saving-and-testing-your-game" class="hash-link" aria-label="Direct link to Saving and testing your game" title="Direct link to Saving and testing your game" translate="no">​</a></h2>
<p>Use <code>PlayerPrefs</code> for typed progress and settings. Call <code>PlayerPrefs.Save()</code>
to persist changes and <code>SaveData.Reload()</code> to load the last save. Check each
result before displaying a success message. Keep keys short: PlayerPrefs
allows 28 bytes per key and 61 bytes per string value.</p>
<p>Test save, load, reset, and retry with <strong>View &gt; Save Data Simulator</strong>, then
repeat those flows in your packaged game. See the
<a class="" href="https://polygonengine.novemtails.com/docs/scripting/savedata">Persistence reference</a> and
<a class="" href="https://polygonengine.novemtails.com/docs/tutorials/two-room-relay/overview">Two-Room Relay tutorial</a>.</p>
<p>For script errors, Debug builds retain source line information. Keep Lua
memory and instruction limits in mind and measure gameplay performance; see
the <a class="" href="https://polygonengine.novemtails.com/docs/scripting/reference">Runtime Reference</a>.</p>
<p><em>Updated for Polygon Engine 0.1.0-ea.2.</em></p>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="Lua" term="Lua"/>
        <category label="Scripting" term="Scripting"/>
        <category label="UI" term="UI"/>
        <category label="Save Data" term="Save Data"/>
        <category label="Runtime" term="Runtime"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The CD-ROM Is Not Extra RAM]]></title>
        <id>https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio</id>
        <link href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A PlayStation disc can hold hundreds of megabytes while the console has only 2 MB of main RAM. That contrast defines the asset pipeline: the disc stores the game, but the runtime still needs explicit loading, bounded buffers, and a plan for seek latency.]]></summary>
        <content type="html"><![CDATA[<p>A PlayStation disc can hold hundreds of megabytes while the console has only 2 MB of main RAM. That contrast defines the asset pipeline: the disc stores the game, but the runtime still needs explicit loading, bounded buffers, and a plan for seek latency.</p>
<p>The CD-ROM is capacity, not working memory.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sector-based-storage">Sector-Based Storage<a href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio#sector-based-storage" class="hash-link" aria-label="Direct link to Sector-Based Storage" title="Direct link to Sector-Based Storage" translate="no">​</a></h2>
<p>Data files are read in sectors. A common data-sector payload is 2,048 bytes, so runtime reads naturally operate in sector-sized units even when a file is not an exact multiple of that size.</p>
<p>This has practical consequences:</p>
<ul>
<li class="">small files still require directory lookup and sector access,</li>
<li class="">unrelated files placed far apart can increase seeking,</li>
<li class="">loading directly inside a frame update can create visible stalls,</li>
<li class="">buffers need room for aligned reads and decoded output,</li>
<li class="">filenames must match the disc filesystem and toolchain rules.</li>
</ul>
<p>Polygon Engine uses short, deterministic disc names and an ISO-friendly directory layout. Human-readable source paths remain in the project; staged runtime paths are compact.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-staged-disc">The Staged Disc<a href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio#the-staged-disc" class="hash-link" aria-label="Direct link to The Staged Disc" title="Direct link to The Staged Disc" translate="no">​</a></h2>
<p>The build output is organized around a small runtime manifest:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">disc_root/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  SYSTEM.CNF</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  POLYGON.EXE</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  DATA/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    MANIFEST.JSN</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    S/  binary scenes</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    L/  Lua scripts</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    M/  meshes</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    R/  materials</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    T/  textures</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    A/  resident audio</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    X/  XA music</span><br></span></code></pre></div></div>
<p><code>SYSTEM.CNF</code> identifies the executable and boot settings. <code>MANIFEST.JSN</code> maps scene names and asset GUIDs to their final disc paths.</p>
<p>The manifest prevents the runtime from reproducing editor logic. It does not scan <code>Assets/</code>, parse import metadata, or guess which file belongs to a GUID. The Windows build resolves those relationships and fails before packaging when a required reference is missing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="loading-is-a-runtime-state">Loading Is a Runtime State<a href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio#loading-is-a-runtime-state" class="hash-link" aria-label="Direct link to Loading Is a Runtime State" title="Direct link to Loading Is a Runtime State" translate="no">​</a></h2>
<p>Reading a scene is not a normal pointer lookup. The runtime has to:</p>
<ol>
<li class="">find the manifest entry,</li>
<li class="">locate the disc file,</li>
<li class="">read it into a bounded buffer,</li>
<li class="">validate its header and size,</li>
<li class="">rebuild runtime tables,</li>
<li class="">upload textures or audio to their dedicated memory,</li>
<li class="">resume gameplay only when the state is coherent.</li>
</ol>
<p>Polygon Engine uses an explicit loading screen during scene transitions. This makes the stall visible and gives the CD subsystem room to complete work without pretending that arbitrary asset access is instantaneous.</p>
<p>The current binary scene read buffer is <code>160 KB</code>; the manifest text buffer is <code>16 KB</code>. Those limits are shared by build validation and runtime code.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="resident-vag-vs-streamed-xa">Resident VAG vs Streamed XA<a href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio#resident-vag-vs-streamed-xa" class="hash-link" aria-label="Direct link to Resident VAG vs Streamed XA" title="Direct link to Resident VAG vs Streamed XA" translate="no">​</a></h2>
<p>The PS1 offers two different audio paths that solve different problems.</p>
<p><strong>Resident effects</strong> are imported from WAV, converted to VAG ADPCM, copied into SPU RAM, and played through SPU voices. They have low playback latency but compete for the 512 KB sound buffer.</p>
<p><strong>Streaming music</strong> is encoded as CD-XA data and read from the disc while it plays. It avoids storing an entire music track in SPU RAM, but it occupies the CD data path.</p>
<p>Polygon Engine exposes both paths:</p>
<table><thead><tr><th>Use</th><th>Import/runtime path</th><th>Main constraint</th></tr></thead><tbody><tr><td>Short effects, loops, voices</td><td>WAV -&gt; VAG -&gt; SPU RAM</td><td>Sound-memory budget and voice use</td></tr><tr><td>Long music</td><td>WAV -&gt; XA -&gt; CD streaming</td><td>Disc bandwidth, seeking, and track limit</td></tr></tbody></table>
<p>The current runtime supports up to <code>16</code> resident audio clips and <code>8</code> XA music tracks. These are bounded engine tables.</p>
<p>During a scene load, Polygon Engine suspends XA playback, performs the disc work, and then restarts the suspended track from its beginning. That policy avoids making the CD-ROM serve two incompatible access patterns at the same moment.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="packaging-is-part-of-correctness">Packaging Is Part of Correctness<a href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio#packaging-is-part-of-correctness" class="hash-link" aria-label="Direct link to Packaging Is Part of Correctness" title="Direct link to Packaging Is Part of Correctness" translate="no">​</a></h2>
<p>The staged files still need a valid disc image. Polygon Engine uses <code>mkpsxiso</code> to generate the package artifacts, including <code>.bin</code>, <code>.cue</code>, and <code>.iso</code> outputs.</p>
<p>The <code>.cue</code> describes the track layout used with the binary image. It is the primary artifact for emulator testing and physical-disc workflows. The standalone <code>.iso</code> is useful for inspection and compatible tools, but it does not replace every track-layout use case.</p>
<p>The package also includes burn guidance because file generation and hardware use are separate concerns. A build that boots in DuckStation proves that the executable and disc structure are coherent. Real hardware remains the final check for media, timing, and drive behavior.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-engine-lesson">The Engine Lesson<a href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio#the-engine-lesson" class="hash-link" aria-label="Direct link to The Engine Lesson" title="Direct link to The Engine Lesson" translate="no">​</a></h2>
<p>Disc architecture reaches far into the editor:</p>
<ul>
<li class="">stable GUIDs make renaming source files safe,</li>
<li class="">import metadata records the compiled artifact,</li>
<li class="">build validation catches missing references,</li>
<li class="">the manifest keeps runtime lookup small,</li>
<li class="">loading screens acknowledge physical latency,</li>
<li class="">XA streaming and scene loading coordinate access to the same device.</li>
</ul>
<p>On PS1, the asset database is not only an editor convenience. It is the front end of a storage system that ends at an optical drive.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline">From Editor JSON to PS1 Runtime Data</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets">Assets, Audio, Lighting, and the Budgets Behind Them</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package">What It Takes to Ship a PS1 Project from an Editor</a></li>
</ul>
<p><em>Updated for Polygon Engine 0.1.0-ea.2.</em></p>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="PS1" term="PS1"/>
        <category label="Hardware" term="Hardware"/>
        <category label="CD-ROM" term="CD-ROM"/>
        <category label="Build" term="Build"/>
        <category label="Audio" term="Audio"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[From Editor JSON to PS1 Runtime Data]]></title>
        <id>https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline</id>
        <link href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline"/>
        <updated>2026-03-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The editor and the PS1 runtime should not consume the same data. That is one of the most important architectural decisions in Polygon Engine.]]></summary>
        <content type="html"><![CDATA[<p>The editor and the PS1 runtime should not consume the same data. That is one of the most important architectural decisions in Polygon Engine.</p>
<p>The editor wants readable project files, flexible authoring state, stable GUIDs, import metadata, screenshots, logs, and panels. The PS1 runtime wants compact data, fixed limits, predictable memory, and short disc paths.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-not-load-editor-data-directly">Why Not Load Editor Data Directly?<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#why-not-load-editor-data-directly" class="hash-link" aria-label="Direct link to Why Not Load Editor Data Directly?" title="Direct link to Why Not Load Editor Data Directly?" translate="no">​</a></h2>
<p>Loading editor JSON directly on PS1 sounds convenient until you count the costs:</p>
<ul>
<li class="">parsing text consumes CPU and memory,</li>
<li class="">large documents need larger buffers,</li>
<li class="">strings and flexible fields complicate memory ownership,</li>
<li class="">missing references fail late,</li>
<li class="">runtime error messages become less actionable,</li>
<li class="">the format becomes hard to change without breaking old projects.</li>
</ul>
<p>Polygon Engine keeps editor data friendly for humans, then converts it during build into runtime data.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="source-of-truth">Source of Truth<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#source-of-truth" class="hash-link" aria-label="Direct link to Source of Truth" title="Direct link to Source of Truth" translate="no">​</a></h2>
<p>The project has several data layers:</p>
<table><thead><tr><th>Layer</th><th>Example path</th><th>Purpose</th></tr></thead><tbody><tr><td>Project config</td><td><code>polygon.project.json</code></td><td>Budgets, scripting limits, build settings, PS1 target settings</td></tr><tr><td>Source assets</td><td><code>Assets/</code></td><td>Models, textures, audio, scripts, materials, Level Builder meshes</td></tr><tr><td>Scenes</td><td><code>Scenes/*.json</code></td><td>Editable scene objects, UI canvas, components</td></tr><tr><td>Imported assets</td><td><code>Library/</code></td><td>Runtime-ready meshes, textures, audio, asset database</td></tr><tr><td>Build output</td><td><code>Build/ps1/</code></td><td>Staged disc root, package files, logs</td></tr></tbody></table>
<p>Only the build output is intended for the PS1 runtime.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="runtime-limits-are-part-of-the-format">Runtime Limits Are Part of the Format<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#runtime-limits-are-part-of-the-format" class="hash-link" aria-label="Direct link to Runtime Limits Are Part of the Format" title="Direct link to Runtime Limits Are Part of the Format" translate="no">​</a></h2>
<p>The runtime uses fixed-size arrays and conservative buffers. Some important limits are shared between editor validation and runtime code:</p>
<table><thead><tr><th>Runtime limit</th><th>Value</th></tr></thead><tbody><tr><td>GameObjects per scene</td><td><code>160</code></td></tr><tr><td>Texture assets</td><td><code>64</code></td></tr><tr><td>Texture page slots</td><td><code>20</code></td></tr><tr><td>Mesh assets</td><td><code>192</code></td></tr><tr><td>Material assets</td><td><code>64</code></td></tr><tr><td>Audio clips</td><td><code>16</code></td></tr><tr><td>Script components</td><td><code>32</code></td></tr><tr><td>Script source cache</td><td><code>32</code></td></tr><tr><td>UI elements</td><td><code>32</code></td></tr><tr><td>Manifest assets</td><td><code>256</code></td></tr><tr><td>Manifest scripts</td><td><code>64</code></td></tr><tr><td>Manifest scenes</td><td><code>32</code></td></tr><tr><td>Manifest size</td><td><code>16 KB</code></td></tr><tr><td>Binary scene size</td><td><code>160 KB</code></td></tr></tbody></table>
<p>These are not arbitrary documentation numbers. They come from the runtime and shared project type limits. The editor validates against them because finding this problem after boot is too late.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-build-stages">What the Build Stages<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#what-the-build-stages" class="hash-link" aria-label="Direct link to What the Build Stages" title="Direct link to What the Build Stages" translate="no">​</a></h2>
<p>A successful PS1 build produces a staged disc root like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Build/ps1/disc_root/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  SYSTEM.CNF</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  POLYGON.EXE</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  DATA/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    MANIFEST.JSN</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    S/  binary scenes</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    L/  Lua scripts</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    M/  meshes</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    R/  materials</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    T/  textures</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    A/  resident audio</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    X/  XA music</span><br></span></code></pre></div></div>
<p>The user-facing output lives under:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Build/ps1/package/</span><br></span></code></pre></div></div>
<p>That package folder contains the generated disc image artifacts and burn guidance. The internal <code>disc_root</code> exists so the pipeline and manifest are inspectable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="manifest-driven-runtime-loading">Manifest-Driven Runtime Loading<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#manifest-driven-runtime-loading" class="hash-link" aria-label="Direct link to Manifest-Driven Runtime Loading" title="Direct link to Manifest-Driven Runtime Loading" translate="no">​</a></h2>
<p>The runtime starts by reading:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">DATA/MANIFEST.JSN</span><br></span></code></pre></div></div>
<p>The manifest maps staged scene, script, and asset references to disc paths. This avoids forcing the runtime to guess where a GUID lives. It also means the build can fail early if a scene references a missing mesh, texture, material, audio clip, or script.</p>
<p>The runtime still has error handling for missing files, bad mesh headers, truncated textures, and script load failures, but the goal is for those errors to be rare in normal editor-built projects.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-tool-requirements">Build Tool Requirements<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#build-tool-requirements" class="hash-link" aria-label="Direct link to Build Tool Requirements" title="Direct link to Build Tool Requirements" translate="no">​</a></h2>
<p>The PS1 build depends on external tools:</p>
<table><thead><tr><th>Tool</th><th>Role</th></tr></thead><tbody><tr><td>Bundled Polygon runtime</td><td>Used by standard binary releases</td></tr><tr><td>PSn00bSDK</td><td>Compiles and links the runtime only in private source builds</td></tr><tr><td>CMake and Ninja</td><td>Generate and run private source runtime builds</td></tr><tr><td>mkpsxiso</td><td>Builds the disc image output</td></tr><tr><td>DuckStation</td><td>Optional Build &amp; Run testing loop</td></tr></tbody></table>
<p>The editor can validate tool paths and report missing or invalid tools. DuckStation is optional unless the user wants one-click testing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-for-users">Why This Matters for Users<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#why-this-matters-for-users" class="hash-link" aria-label="Direct link to Why This Matters for Users" title="Direct link to Why This Matters for Users" translate="no">​</a></h2>
<p>A user should not have to understand the internal runtime to make a working build. They should see:</p>
<ul>
<li class="">which scenes are included,</li>
<li class="">which scene starts first,</li>
<li class="">which assets are missing,</li>
<li class="">which budgets are over limit,</li>
<li class="">which scripts exceed runtime constraints,</li>
<li class="">which colliders or materials are risky.</li>
</ul>
<p>That is why the Problems panel and PS1 Budget dashboard are part of the pipeline. The build system is not just a compiler wrapper. It is the place where editor freedom becomes PS1-compatible runtime data.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-design-tradeoff">The Design Tradeoff<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#the-design-tradeoff" class="hash-link" aria-label="Direct link to The Design Tradeoff" title="Direct link to The Design Tradeoff" translate="no">​</a></h2>
<p>This approach adds work to the build pipeline. It has to convert, validate, stage, and package. But it keeps the runtime simpler and more predictable.</p>
<p>On PS1, that tradeoff is worth it. Complexity belongs in the Windows editor whenever possible. The console runtime should spend its limited memory and CPU budget on the game.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation">A Practical Map of the Original PlayStation</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/cd-rom-data-paths-and-xa-audio">The CD-ROM Is Not Extra RAM</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/building-a-shippable-ps1-package">What It Takes to Ship a PS1 Project from an Editor</a></li>
</ul>
<p><em>Updated for Polygon Engine 0.1.0-ea.2.</em></p>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="Editor" term="Editor"/>
        <category label="Workflow" term="Workflow"/>
        <category label="PS1" term="PS1"/>
        <category label="Build" term="Build"/>
        <category label="Runtime" term="Runtime"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building PS1-Safe Levels from Editor Geometry]]></title>
        <id>https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring</id>
        <link href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring"/>
        <updated>2026-02-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Polygon Engine started with a familiar idea: let the user build a level from visual objects in an editor. The PS1 immediately pushes back on that idea. A scaled cube in an editor is easy. A scaled cube that sorts correctly, clips correctly, collides correctly, and packages correctly for PS1 is a much bigger requirement.]]></summary>
        <content type="html"><![CDATA[<p>Polygon Engine started with a familiar idea: let the user build a level from visual objects in an editor. The PS1 immediately pushes back on that idea. A scaled cube in an editor is easy. A scaled cube that sorts correctly, clips correctly, collides correctly, and packages correctly for PS1 is a much bigger requirement.</p>
<p>This is why level authoring in Polygon Engine is split into two related paths: scene primitives and reusable Level Builder mesh assets.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="scene-primitives-vs-mesh-assets">Scene Primitives vs Mesh Assets<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#scene-primitives-vs-mesh-assets" class="hash-link" aria-label="Direct link to Scene Primitives vs Mesh Assets" title="Direct link to Scene Primitives vs Mesh Assets" translate="no">​</a></h2>
<p>The editor supports built-in scene primitives such as <code>Plane</code>, <code>Cube</code>, and <code>Capsule</code>. They are useful for fast blockouts and simple level structure.</p>
<p>The Level Builder creates editable mesh assets. Those are saved as project-owned source meshes and compiled into runtime mesh files.</p>
<p>That distinction matters:</p>
<table><thead><tr><th>Authoring path</th><th>Source location</th><th>Runtime output</th><th>Best use</th></tr></thead><tbody><tr><td>Built-in scene primitive</td><td>Scene JSON</td><td>Baked/staged mesh when compatible</td><td>Floors, walls, blockers</td></tr><tr><td>Level Builder mesh</td><td><code>Assets/Meshes/*.pmsh</code></td><td><code>Library/meshes/*.msh</code></td><td>Reusable custom pieces</td></tr><tr><td>Imported model</td><td><code>Assets/Models/*.obj</code> or <code>.fbx</code></td><td><code>Library/meshes/*.msh</code></td><td>External low-poly art</td></tr></tbody></table>
<p>All of them can end up as runtime mesh data, but they have different editing and validation rules.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bake-path">The Bake Path<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#the-bake-path" class="hash-link" aria-label="Direct link to The Bake Path" title="Direct link to The Bake Path" translate="no">​</a></h2>
<p>The PS1 build pipeline can process compatible static built-in <code>Plane</code> and <code>Cube</code> geometry. The goal is to avoid shipping huge raw primitives to a renderer that has no z-buffer.</p>
<p>The bake path uses constants that are deliberately conservative:</p>
<table><thead><tr><th>Setting</th><th>Current value</th><th>Purpose</th></tr></thead><tbody><tr><td>Mesh bake quantization scale</td><td><code>4096.0</code></td><td>Mesh-local bake representation; distinct from runtime world-position scale</td></tr><tr><td>Default cell size</td><td><code>1.0</code></td><td>Split large surfaces into safer cells</td></tr><tr><td>Default chunk size</td><td><code>3.0</code></td><td>Keep staged chunks manageable</td></tr><tr><td>Min cell size</td><td><code>0.125</code></td><td>Avoid accidental over-subdivision</td></tr><tr><td>Max segments</td><td><code>32</code></td><td>Bound generated geometry</td></tr><tr><td>Visibility grouping</td><td>Build-derived</td><td>Group baked geometry for conservative runtime candidate selection</td></tr></tbody></table>
<p>The exact grouping and runtime cell size are implementation details and can
change independently as the visibility path is tuned. Do not build gameplay
logic around a fixed sector dimension. The stable authoring rule is that large
primitives should become smaller runtime surfaces when that is safer for the
ordering table and visibility workload.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="covered-faces-and-conservative-removal">Covered Faces and Conservative Removal<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#covered-faces-and-conservative-removal" class="hash-link" aria-label="Direct link to Covered Faces and Conservative Removal" title="Direct link to Covered Faces and Conservative Removal" translate="no">​</a></h2>
<p>Adjacent boxes often contain faces the player never sees: wall-to-wall contact faces, floor/wall overlaps, or hidden backsides inside modular construction. Removing some covered faces helps performance and reduces ordering conflicts.</p>
<p>Polygon Engine's removal is intentionally conservative. It should never behave like a full constructive-solid-geometry system. It does not solve arbitrary mesh boolean operations, and it does not claim to optimize every imported model.</p>
<p>The useful target is narrower:</p>
<ul>
<li class="">remove obvious covered faces for compatible static primitive layouts,</li>
<li class="">preserve material assignment,</li>
<li class="">preserve world placement,</li>
<li class="">avoid deleting faces when the decision is ambiguous.</li>
</ul>
<p>For a PS1 editor, a safe partial optimization is better than a broad optimization that silently breaks levels.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="level-builder-mesh-assets">Level Builder Mesh Assets<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#level-builder-mesh-assets" class="hash-link" aria-label="Direct link to Level Builder Mesh Assets" title="Direct link to Level Builder Mesh Assets" translate="no">​</a></h2>
<p>Level Builder is the in-editor path for authoring reusable mesh pieces. The workflow is:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Create Primitive / Load Mesh Asset</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; edit in Object, Vertex, Edge, or Face mode</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; validate mesh</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; save source .pmsh under Assets/Meshes/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; export runtime .msh under Library/meshes/</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; register GUID in Library/asset_db.json</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; place mesh in scene</span><br></span></code></pre></div></div>
<p>The compiled mesh runtime format uses the <code>PGMSH1</code> magic. The runtime validates the header, vertex count, index count, index range, and data size before using the mesh.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="topology-editing-is-a-product-feature">Topology Editing Is a Product Feature<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#topology-editing-is-a-product-feature" class="hash-link" aria-label="Direct link to Topology Editing Is a Product Feature" title="Direct link to Topology Editing Is a Product Feature" translate="no">​</a></h2>
<p>Moving a face should not accidentally detach it from the model. Pulling an edge should not move only one triangle unless the tool is explicitly separating or extruding geometry.</p>
<p>That sounds obvious, but it is one of the most important pieces of a trustworthy level editor. If a non-engine-programmer user can create broken topology without realizing it, the engine is not production-ready.</p>
<p>Level Builder therefore needs clear tool behavior:</p>
<ul>
<li class="">selection modes must be explicit,</li>
<li class="">gizmo drags must transform the selected geometry, not the camera,</li>
<li class="">shared vertices and edges should remain shared by default,</li>
<li class="">destructive operations such as Delete, Duplicate, Extrude, Inset, Merge, and Subdivide should be intentional,</li>
<li class="">validation should surface degenerate faces, long edges, and non-manifold risk.</li>
</ul>
<p>The modeling tool does not need to compete with Blender. It needs to produce reliable PS1 gameplay geometry.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="colliders-are-not-optional">Colliders Are Not Optional<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#colliders-are-not-optional" class="hash-link" aria-label="Direct link to Colliders Are Not Optional" title="Direct link to Colliders Are Not Optional" translate="no">​</a></h2>
<p>Visual geometry and collision geometry are separate concepts. A mesh can render correctly and still let the player walk through it if the scene has no Collider component.</p>
<p>When a Level Builder mesh is placed into a scene, Polygon Engine creates a GameObject with:</p>
<ul>
<li class="">Mesh Renderer referencing the saved mesh asset,</li>
<li class="">an AABB Collider based on the mesh bounds.</li>
</ul>
<p>That collider can be edited in the main Inspector. The build validation also warns when a Level Builder mesh reaches the PS1 runtime path without a Collider, because that object can be walked through.</p>
<p>This is a good example of product-oriented validation. The warning explains the gameplay consequence, not only the data problem.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="authoring-rules-that-matter">Authoring Rules That Matter<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#authoring-rules-that-matter" class="hash-link" aria-label="Direct link to Authoring Rules That Matter" title="Direct link to Authoring Rules That Matter" translate="no">​</a></h2>
<p>For PS1-facing levels, good content is usually:</p>
<ul>
<li class="">modular,</li>
<li class="">low-poly,</li>
<li class="">chunked into rooms, corridors, and thresholds,</li>
<li class="">built from simple opaque materials,</li>
<li class="">explicit about collision,</li>
<li class="">validated before packaging.</li>
</ul>
<p>Bad content is usually:</p>
<ul>
<li class="">giant coplanar surfaces,</li>
<li class="">thin overlapping shells,</li>
<li class="">dense rounded primitives used everywhere,</li>
<li class="">transparent surfaces placed across the main view,</li>
<li class="">imported art with no collider or budget review.</li>
</ul>
<p>Polygon Engine's authoring workflow exists to move users from the second list to the first without asking them to understand every PS1 hardware detail.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints">Rendering for a Console Without a Z-Buffer</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline">From Editor JSON to PS1 Runtime Data</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/docs/editor/level-builder">Level Builder reference</a></li>
</ul>
<p><em>Updated for Polygon Engine 0.1.0-ea.2.</em></p>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="PS1" term="PS1"/>
        <category label="Editor" term="Editor"/>
        <category label="Authoring" term="Authoring"/>
        <category label="Primitives" term="Primitives"/>
        <category label="Build" term="Build"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Rendering for a Console Without a Z-Buffer]]></title>
        <id>https://polygonengine.novemtails.com/blog/ps1-rendering-constraints</id>
        <link href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints"/>
        <updated>2026-02-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The original PlayStation is not just a slow modern GPU. It is a different rendering model. That difference is the reason Polygon Engine treats PS1 rendering as a content and tooling problem, not only as a runtime problem.]]></summary>
        <content type="html"><![CDATA[<p>The original PlayStation is not just a slow modern GPU. It is a different rendering model. That difference is the reason Polygon Engine treats PS1 rendering as a content and tooling problem, not only as a runtime problem.</p>
<p>The editor can show a clean 3D scene. The PS1 runtime has to turn that scene into ordered GPU primitives with no z-buffer, fixed-point transforms, small memory pools, and a very unforgiving camera near plane.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-core-constraint">The Core Constraint<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#the-core-constraint" class="hash-link" aria-label="Direct link to The Core Constraint" title="Direct link to The Core Constraint" translate="no">​</a></h2>
<p>Modern engines usually rely on a z-buffer. If two triangles overlap on screen, the GPU can compare depth per pixel and keep the closest fragment. The PS1 GPU does not do that.</p>
<p>The PS1 pipeline is closer to this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">model vertices</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; GTE transform and projection</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; depth estimate</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; ordering table bucket</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; GPU primitive packet</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; draw in ordered submission</span><br></span></code></pre></div></div>
<p>That means visibility is approximate. A triangle is not resolved per pixel against every other triangle. The engine picks an ordering bucket, inserts a primitive, and the GPU draws it when that bucket is processed.</p>
<p>Polygon Engine's runtime uses an ordering table length of <code>2048</code>, with the first <code>64</code> buckets reserved for UI and foreground work. That gives a useful depth range, but it does not make arbitrary overlapping geometry safe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-editor-geometry-lies">Why Editor Geometry Lies<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#why-editor-geometry-lies" class="hash-link" aria-label="Direct link to Why Editor Geometry Lies" title="Direct link to Why Editor Geometry Lies" translate="no">​</a></h2>
<p>An editor viewport can make bad PS1 content look fine:</p>
<ul>
<li class="">giant floors look stable because the preview can sort or rasterize more forgivingly,</li>
<li class="">overlapping walls look acceptable because the desktop path can hide depth ambiguity,</li>
<li class="">thin coplanar details look crisp until the PS1 ordering table flips them,</li>
<li class="">very close walls look fine until they cross the near plane,</li>
<li class="">transparent materials look correct until draw order becomes content-dependent.</li>
</ul>
<p>The practical lesson is that PS1 scenes need authoring rules. The renderer alone cannot make every scene correct.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ordering-table-policy-in-polygon-engine">Ordering Table Policy in Polygon Engine<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#ordering-table-policy-in-polygon-engine" class="hash-link" aria-label="Direct link to Ordering Table Policy in Polygon Engine" title="Direct link to Ordering Table Policy in Polygon Engine" translate="no">​</a></h2>
<p>The runtime estimates depth and clamps primitives into ordering table buckets. It also computes GTE ordering scale values from the configured far distance, because a very large far plane spreads limited depth precision across too much space.</p>
<p>That is why the editor warns when PS1 far distance goes above roughly <code>32</code> world units. A larger range can be useful, but it reduces ordering precision and makes polygon overlap more visible.</p>
<p>The runtime currently combines several strategies:</p>
<table><thead><tr><th>Problem</th><th>Runtime/editor response</th></tr></thead><tbody><tr><td>Depth buckets are finite</td><td>Clamp primitives into a bounded ordering table</td></tr><tr><td>UI must stay readable</td><td>Reserve foreground buckets for authored UI</td></tr><tr><td>Large surfaces sort poorly</td><td>Warn and bake compatible static geometry into smaller chunks</td></tr><tr><td>Mesh triangles can conflict internally</td><td>Use per-mesh triangle sorting when appropriate</td></tr><tr><td>Transparent/double-sided surfaces are fragile</td><td>Validate and warn in the editor</td></tr></tbody></table>
<p>This is not a full visibility solver. It is a PS1-appropriate compromise.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="near-plane-clipping">Near-Plane Clipping<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#near-plane-clipping" class="hash-link" aria-label="Direct link to Near-Plane Clipping" title="Direct link to Near-Plane Clipping" translate="no">​</a></h2>
<p>One of the most obvious PS1 rendering failures is a wall face disappearing when the player gets close. This happens when a primitive crosses the camera near plane and the renderer either rejects it too aggressively or projects invalid coordinates.</p>
<p>Polygon Engine's runtime handles this with conservative near-plane policy:</p>
<ul>
<li class="">keep mesh chunks alive when their bounds touch the near plane,</li>
<li class="">let per-triangle clipping decide what survives,</li>
<li class="">clip triangles and quads against the complete visible frustum,</li>
<li class="">use safe projected coordinates when vertices get too close,</li>
<li class="">prevent lateral grazing from collapsing extreme coordinates into the GPU's
signed screen-coordinate range,</li>
<li class="">avoid relying on a coarse object cull for close camera geometry.</li>
</ul>
<p>That work was necessary for first-person scenes. In a first-person game, the camera constantly touches walls, terminals, pickups, and doors. Near-plane clipping is not an edge case; it is part of normal play.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="large-triangles-are-a-content-bug">Large Triangles Are a Content Bug<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#large-triangles-are-a-content-bug" class="hash-link" aria-label="Direct link to Large Triangles Are a Content Bug" title="Direct link to Large Triangles Are a Content Bug" translate="no">​</a></h2>
<p>The PS1 can draw large polygons, but large polygons are harder to order. A single wall quad that spans a whole corridor can overlap many objects and still produce one approximate depth value. The larger the primitive, the more likely the order is wrong for part of the screen.</p>
<p>Polygon Engine pushes against that in two places:</p>
<ol>
<li class=""><strong>Authoring guidance</strong>: use smaller modular pieces for floors, walls, and thresholds.</li>
<li class=""><strong>Build processing</strong>: compatible static <code>Plane</code> and <code>Cube</code> geometry can be baked into smaller staged mesh chunks.</li>
</ol>
<p>The runtime also has a triangle sorting cap for mesh-local sorting. That cap keeps sorting useful without letting sorting itself become the frame-time problem.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-supported-boundary">The Supported Boundary<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#the-supported-boundary" class="hash-link" aria-label="Direct link to The Supported Boundary" title="Direct link to The Supported Boundary" translate="no">​</a></h2>
<p>Polygon Engine does not claim to provide:</p>
<ul>
<li class="">it does not add a z-buffer to PS1,</li>
<li class="">it does not make arbitrary interpenetrating geometry correct,</li>
<li class="">it does not turn every imported model into a PS1-safe layout,</li>
<li class="">it does not make transparency order-independent,</li>
<li class="">it does not remove the need for content budgets.</li>
</ul>
<p>The design goal is narrower and more useful: provide a visual editor while keeping final geometry inside a PS1-shaped envelope.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-better-mental-model">A Better Mental Model<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#a-better-mental-model" class="hash-link" aria-label="Direct link to A Better Mental Model" title="Direct link to A Better Mental Model" translate="no">​</a></h2>
<p>Think of PS1 rendering as a contract between the editor, build pipeline, and runtime:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Editor:</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  make risky geometry visible and editable</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Build:</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  bake, chunk, validate, and stage runtime data</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Runtime:</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  transform, clip, sort, and draw with predictable limits</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">User:</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  keep content modular, simple, and target-aware</span><br></span></code></pre></div></div>
<p>That is the approach behind Polygon Engine. The PS1 is not a platform where the renderer can clean up every content mistake at the end. The tool has to guide the author before the runtime gets involved.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation">A Practical Map of the Original PlayStation</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts">VRAM Is a Layout, Not a Texture Folder</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/ps1-safe-level-authoring">Building PS1-Safe Levels from Editor Geometry</a></li>
</ul>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="PS1" term="PS1"/>
        <category label="Rendering" term="Rendering"/>
        <category label="GPU" term="GPU"/>
        <category label="Sorting" term="Sorting"/>
        <category label="Optimization" term="Optimization"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[VRAM Is a Layout, Not a Texture Folder]]></title>
        <id>https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts</id>
        <link href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts"/>
        <updated>2026-01-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The original PlayStation has 1 MB of VRAM, but treating that number as a simple texture budget is misleading. VRAM is a 1024 x 512 field of 16-bit words shared by display buffers, texture data, and color lookup tables.]]></summary>
        <content type="html"><![CDATA[<p>The original PlayStation has 1 MB of VRAM, but treating that number as a simple texture budget is misleading. VRAM is a 1024 x 512 field of 16-bit words shared by display buffers, texture data, and color lookup tables.</p>
<p>Every rendering decision changes that layout. Resolution, color depth, double buffering, texture format, and palette placement all consume the same address space.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start-with-the-display-buffers">Start with the Display Buffers<a href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts#start-with-the-display-buffers" class="hash-link" aria-label="Direct link to Start with the Display Buffers" title="Direct link to Start with the Display Buffers" translate="no">​</a></h2>
<p>Polygon Engine targets a 320 x 240 frame. In 15-bit display mode, one frame occupies approximately:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">320 x 240 x 2 bytes = 153,600 bytes</span><br></span></code></pre></div></div>
<p>Double buffering needs two display areas, so about 307,200 bytes are committed before textures enter the picture. Alignment, drawing regions, and the chosen VRAM arrangement matter in addition to the raw byte count.</p>
<p>The two buffers solve a different problem from a z-buffer. One is displayed while the GPU draws the next frame into the other. Swapping them prevents the viewer from seeing a partially rendered frame. Neither buffer stores per-pixel depth.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="texture-pages">Texture Pages<a href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts#texture-pages" class="hash-link" aria-label="Direct link to Texture Pages" title="Direct link to Texture Pages" translate="no">​</a></h2>
<p>The GPU addresses texture data through texture-page state plus per-vertex <code>u, v</code> coordinates. A texture page represents a 256 x 256 texel coordinate space, but its width in VRAM words depends on pixel format:</p>
<table><thead><tr><th>Texture format</th><th style="text-align:right">Bits per texel</th><th style="text-align:right">Texels per 16-bit VRAM word</th><th>Palette required</th></tr></thead><tbody><tr><td>4-bit indexed</td><td style="text-align:right">4</td><td style="text-align:right">4</td><td>16-color CLUT</td></tr><tr><td>8-bit indexed</td><td style="text-align:right">8</td><td style="text-align:right">2</td><td>256-color CLUT</td></tr><tr><td>15-bit direct</td><td style="text-align:right">16 stored bits</td><td style="text-align:right">1</td><td>No</td></tr></tbody></table>
<p>Indexed textures trade palette restrictions for density. A 4-bit texture stores four texels in the same VRAM width used by one direct-color texel. That can be a major saving when the art works with a small palette.</p>
<p>The page is not a standalone texture object. Several imported images can occupy regions within a page, provided their coordinates, format, and CLUT references remain valid.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cluts-are-real-vram-data">CLUTs Are Real VRAM Data<a href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts#cluts-are-real-vram-data" class="hash-link" aria-label="Direct link to CLUTs Are Real VRAM Data" title="Direct link to CLUTs Are Real VRAM Data" translate="no">​</a></h2>
<p>A CLUT is a row of 16-bit colors stored in VRAM. Four-bit textures select among 16 entries; eight-bit textures select among 256.</p>
<p>This creates useful art techniques:</p>
<ul>
<li class="">reuse one indexed image with different palettes,</li>
<li class="">keep UI icons compact,</li>
<li class="">reserve direct color for gradients or assets that cannot share a palette.</li>
</ul>
<p>It also creates failure cases. A texture can fit while its palette rows do not. Two images can overlap because their texel dimensions were converted to VRAM words incorrectly. A CLUT placed inside a display area can be overwritten by rendering.</p>
<p>VRAM validation has to understand placement, not only file size.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-textures-warp">Why Textures Warp<a href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts#why-textures-warp" class="hash-link" aria-label="Direct link to Why Textures Warp" title="Direct link to Why Textures Warp" translate="no">​</a></h2>
<p>The GPU interpolates texture coordinates affinely in screen space. It does not perform perspective-correct interpolation. A large polygon viewed at an angle therefore shows the familiar PS1 texture swim or warp.</p>
<p>More texture resolution does not fix the interpolation model. The practical controls are geometric:</p>
<ul>
<li class="">keep walls and floors modular,</li>
<li class="">avoid one polygon spanning a large depth range,</li>
<li class="">subdivide surfaces where the artifact is unacceptable,</li>
<li class="">keep UV seams and T-junctions under control,</li>
<li class="">use the visual character intentionally instead of expecting modern stability.</li>
</ul>
<p>Subdivision improves local interpolation but increases transformed vertices, primitives, packet memory, and ordering work. It is always a budget trade.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="polygon-engines-import-contract">Polygon Engine's Import Contract<a href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts#polygon-engines-import-contract" class="hash-link" aria-label="Direct link to Polygon Engine's Import Contract" title="Direct link to Polygon Engine's Import Contract" translate="no">​</a></h2>
<p>Polygon Engine converts source images into PS1-oriented <code>.tex</code> data. Import metadata records the format, dimensions, palette requirements, and estimated VRAM use. The build then performs deterministic page and CLUT packing.</p>
<p>The current runtime supports:</p>
<ul>
<li class="">up to <code>64</code> texture assets,</li>
<li class=""><code>20</code> texture-page slots in the allocator,</li>
<li class="">project-visible VRAM placement and budget validation.</li>
</ul>
<p>Those are engine limits, not claims about the theoretical maximum of the hardware. They keep runtime tables and the build allocator bounded.</p>
<p>If packing fails, the useful question is not only "Are my textures too large?" It is also:</p>
<ul>
<li class="">Are too many assets using direct color?</li>
<li class="">Can several images share a palette?</li>
<li class="">Are UI textures consuming world-texture space?</li>
<li class="">Is an image reserving a large rectangular region with little useful content?</li>
<li class="">Does the chosen display layout leave the expected free region?</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dithering-and-the-final-image">Dithering and the Final Image<a href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts#dithering-and-the-final-image" class="hash-link" aria-label="Direct link to Dithering and the Final Image" title="Direct link to Dithering and the Final Image" translate="no">​</a></h2>
<p>The GPU performs color calculations with more precision than the 15-bit display result and can apply dithering when reducing the output. Polygon Engine exposes dithering because it helps gradients and lighting survive the final color quantization.</p>
<p>Dithering does not create more VRAM or remove banding in every case. It changes the spatial pattern of the error, which often reads better on low-resolution output.</p>
<p>VRAM is one of the clearest examples of PS1 development as systems work. Texture import, screen setup, material design, UI art, and runtime rendering all meet in the same 1 MB map.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation">A Practical Map of the Original PlayStation</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints">Rendering for a Console Without a Z-Buffer</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/audio-lighting-and-runtime-budgets">Assets, Audio, Lighting, and the Budgets Behind Them</a></li>
</ul>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="PS1" term="PS1"/>
        <category label="Hardware" term="Hardware"/>
        <category label="GPU" term="GPU"/>
        <category label="Rendering" term="Rendering"/>
        <category label="VRAM" term="VRAM"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A Practical Map of the Original PlayStation]]></title>
        <id>https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation</id>
        <link href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation"/>
        <updated>2026-01-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The original PlayStation is a group of specialized processors connected by narrow, explicit data paths. The CPU runs game code, the GTE accelerates geometry math, the GPU draws 2D primitives, the SPU plays sound, and the CD-ROM supplies data that cannot remain in memory.]]></summary>
        <content type="html"><![CDATA[<p>The original PlayStation is a group of specialized processors connected by narrow, explicit data paths. The CPU runs game code, the GTE accelerates geometry math, the GPU draws 2D primitives, the SPU plays sound, and the CD-ROM supplies data that cannot remain in memory.</p>
<p>Understanding those boundaries explains most PS1 engine decisions. The machine is fast when work is prepared for the right unit and expensive when the CPU has to compensate for a poor data layout.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-hardware-at-a-glance">The Hardware at a Glance<a href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation#the-hardware-at-a-glance" class="hash-link" aria-label="Direct link to The Hardware at a Glance" title="Direct link to The Hardware at a Glance" translate="no">​</a></h2>
<table><thead><tr><th>Part</th><th>Relevant capability</th><th>Engine consequence</th></tr></thead><tbody><tr><td>CPU</td><td>Customized 32-bit R3000A-class RISC processor at 33.8688 MHz</td><td>Gameplay and orchestration need short, predictable paths</td></tr><tr><td>Main RAM</td><td>2 MB</td><td>Runtime code, scene data, meshes, Lua, collision, and transient buffers compete directly</td></tr><tr><td>GTE</td><td>Fixed-point vector, matrix, projection, and lighting coprocessor</td><td>Transform data must fit its numeric range and instruction model</td></tr><tr><td>GPU</td><td>Polygon, sprite, line, image-transfer, blending, and dithering commands</td><td>The CPU submits packets; the GPU does not consume a modern vertex buffer</td></tr><tr><td>VRAM</td><td>1 MB, arranged as a 1024 x 512 field of 16-bit words</td><td>Display buffers, textures, and color lookup tables share one layout</td></tr><tr><td>SPU</td><td>24 ADPCM voices with 512 KB of sound RAM</td><td>Resident effects must be compressed and budgeted separately from streamed audio</td></tr><tr><td>CD-ROM</td><td>Sector-based optical storage</td><td>Files are plentiful, but seeking and loading are not free</td></tr></tbody></table>
<p>The CPU also has a 4 KB instruction cache and a 1 KB scratchpad. Those numbers are small enough that code structure and data locality are part of performance work.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-frame-crosses-several-systems">One Frame Crosses Several Systems<a href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation#one-frame-crosses-several-systems" class="hash-link" aria-label="Direct link to One Frame Crosses Several Systems" title="Direct link to One Frame Crosses Several Systems" translate="no">​</a></h2>
<p>A typical 3D frame is not sent to a single graphics API. It moves through a chain:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">gameplay and scene traversal on the CPU</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; world/view matrices</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; GTE transform, lighting, and perspective projection</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; screen-space triangles and quads</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; depth buckets in an ordering table</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; GPU command packets in main RAM</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; DMA submission to the GPU</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; rasterization into VRAM</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -&gt; video output from the current display area</span><br></span></code></pre></div></div>
<p>The GTE does not draw pixels. The GPU does not transform 3D vertices. The ordering table is a CPU-side structure that decides packet submission order. Each unit solves a specific part of the frame.</p>
<p>This separation is why a PS1 renderer needs more than a triangle loop. It needs bounded packet memory, numeric conversion, clipping, culling, ordering policy, double buffering, and a VRAM layout that remains valid for the entire frame.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="memory-is-split-by-purpose">Memory Is Split by Purpose<a href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation#memory-is-split-by-purpose" class="hash-link" aria-label="Direct link to Memory Is Split by Purpose" title="Direct link to Memory Is Split by Purpose" translate="no">​</a></h2>
<p>The PS1 has no unified memory pool.</p>
<ul>
<li class="">Main RAM holds executable code and live game data.</li>
<li class="">VRAM holds display areas, texture pixels, and CLUT data.</li>
<li class="">SPU RAM holds resident ADPCM samples.</li>
<li class="">CD-ROM holds the complete game but behaves as storage, not working memory.</li>
</ul>
<p>Moving data between these domains is an operation. Textures are uploaded to VRAM. VAG samples are transferred to SPU RAM. Scene and asset files are read from disc into bounded main-memory buffers.</p>
<p>An engine therefore needs several budgets at once. A scene can fit in main RAM and still fail because its textures do not pack into VRAM. It can fit both and still exhaust SPU RAM with resident audio.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="fixed-point-is-part-of-the-architecture">Fixed Point Is Part of the Architecture<a href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation#fixed-point-is-part-of-the-architecture" class="hash-link" aria-label="Direct link to Fixed Point Is Part of the Architecture" title="Direct link to Fixed Point Is Part of the Architecture" translate="no">​</a></h2>
<p>The console does not include the usual floating-point coprocessor. A compiler can provide software floating point, but it is a poor default for frame-critical work. PS1 graphics code instead relies heavily on integer and fixed-point representations.</p>
<p>Fixed point is not merely a performance trick. It changes the failure modes:</p>
<ul>
<li class="">values have explicit scale and range,</li>
<li class="">multiplication needs wider intermediates or careful shifts,</li>
<li class="">projection can saturate,</li>
<li class="">world coordinates can overflow the range expected by the GTE,</li>
<li class="">vertices snap to the integer screen grid.</li>
</ul>
<p>Polygon Engine keeps editor transforms convenient on Windows, then converts and validates them for the runtime. Geometry that approaches unsafe projection ranges uses a guarded clipping/projection path instead of trusting saturated GTE output.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-polygon-engine-does-more-work-before-boot">Why Polygon Engine Does More Work Before Boot<a href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation#why-polygon-engine-does-more-work-before-boot" class="hash-link" aria-label="Direct link to Why Polygon Engine Does More Work Before Boot" title="Direct link to Why Polygon Engine Does More Work Before Boot" translate="no">​</a></h2>
<p>The Windows editor has memory, file-system access, descriptive errors, and enough CPU time for conversion. The console should receive compact data that already respects runtime limits.</p>
<p>Polygon Engine uses that asymmetry deliberately:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">Windows editor  -&gt; import, author, inspect</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Build pipeline  -&gt; validate, quantize, bake, stage, package</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">PS1 runtime     -&gt; load bounded data and execute the game</span><br></span></code></pre></div></div>
<p>This is the central architectural rule of the engine. Flexible source data stays on the development machine. Predictable binary data goes to the console.</p>
<p>The result is not an attempt to hide the PlayStation. It is a workflow that exposes the constraints early enough to act on them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="continue-reading">Continue Reading<a href="https://polygonengine.novemtails.com/blog/practical-map-of-the-original-playstation#continue-reading" class="hash-link" aria-label="Direct link to Continue Reading" title="Direct link to Continue Reading" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/ps1-vram-texture-pages-and-cluts">VRAM Is a Layout, Not a Texture Folder</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/ps1-rendering-constraints">Rendering for a Console Without a Z-Buffer</a></li>
<li class=""><a class="" href="https://polygonengine.novemtails.com/blog/editor-to-runtime-pipeline">From Editor JSON to PS1 Runtime Data</a></li>
</ul>]]></content>
        <author>
            <name>Novemtails</name>
            <uri>https://novemtails.com</uri>
        </author>
        <category label="PS1" term="PS1"/>
        <category label="Hardware" term="Hardware"/>
        <category label="Architecture" term="Architecture"/>
        <category label="Runtime" term="Runtime"/>
    </entry>
</feed>