Version: 0.0.9 (beta)
Edit NES graphics in game-aware context instead of rebuilding screens by hand.
PPUX uses an in-app database plus project files to understand banks, palettes, sprite layouts, animations, and other ROM-specific structures.
ℹ️ If you wish to support the project, you can do so here.
Create a folder, place your ROM inside it, then drag the ROM into PPUX. After that, the app will either:
- Open a default layout
- Open a DB-backed game layout
- Load an existing user project (
.luaor.ppux)
If a ROM has no DB entry yet, it can still be used normally. DB entries are just curated starting points.
Example with an animation for Dr. Mario, this can be done in 2 or 3 minutes:
Windows are the main work areas in PPUX. Some are source windows, some are layout windows, and some are ROM-backed helper windows.
Notes:
-
To be clearer on CHR vs ROM windows: CHR Banks is the normal source browser containing only graphics data.
-
ROM Banks is the fallback source browser, useful for Games that use CHR RAM data (like Megaman 2, for instance) and, as mentioned above, it will load the whole ROM, so be careful on unintentional non-graphics pixel edits.
-
Not all window types can currently be created through the UI. ROM-backed windows that depend on ROM addresses and related metadata still need to be created manually in Lua project files for now, but this will be improved.
Palette windows are the palette editors/viewers used by the rest of the app.
There are 2 kinds:
Global palette: the fallback palette for content that does not have a ROM palette linked to it. Use this for mockups, freeform art, and anything with no specific in-game palette assigned.ROM palette: a real 4x4 palette window backed by ROM data. It can be linked to specific windows and layers, to use the actual in-game palette through palette links.
In practice:
- If an item or layer has no ROM palette assigned, it uses a
Global palette. - If you want the colors to reflect actual game palette bytes, use a
ROM palette. - Only
ROM palettewindows are meant to be linked to other windows. - Palette row numbers
1to4select the row used by layers/items that support palette-number selection. - Click a color to select it for editing/painting.
- In palette windows, arrow keys move the selected cell.
- In palette windows,
Shift + arrows, mouse wheel, andShift + mouse wheeladjust colors.
| Normal mode | Compact mode | |
|---|---|---|
| Global palette | ![]() |
![]() |
| ROM palette | ![]() |
![]() |
To link a ROM palette to another window, drag from the small pivot button in the ROM palette toolbar into the destination window. This is the UI-driven way to create palette links between a ROM palette and a window/layer.
Ctrl + 1/2/3: change app scaleCtrl + F: toggle fullscreenCtrl + N: openNew WindowCtrl + S: open save optionsTab: toggleTile/EditmodeCtrl + G: toggle the focused window gridCtrl + R: toggle shader rendering for the focused layerCtrl + Z/Ctrl + Y: undo / redoRight clickormiddle clickdrag: move windows- taskbar: focus, restore, and manage windows
Tile mode is for selection, drag and drop and tile-level editing in general.
- left click to select
Ctrl + clickorShift + dragfor multi-selectionCtrl + Ato select allDelete/Backspaceto remove selection where supported- arrows to move tile selections
Shift + Up/Downto switch layersCtrl + Up/Downto change inactive-layer opacity1to4to assign palette numbers where supportedH/Vto mirror selected sprites- bank windows:
Left/Rightswitch banks,Mtoggles8x8/8x16
Edit mode is for pixel-level editing.
- left click to paint
Shift + clickdraws a line from the last painted/clicked pointRtoggles the rectangle fill tool- hold
Gand left click or drag to grab a color - hold
Fand left click to flood fill 1to4to choose the active colorAlt + 1/2/3/4to change brush size presetsCtrl + Alt + mouse wheelalso changes brush sizeCtrl + Rtoggles shader rendering for the focused layerCtrl + Gtoggles the focused window gridCtrl + Z/Ctrl + Y: undo / redo
You can drag and drop a PNG directly into PPUX. What happens depends on the window under the mouse, and sometimes on the focused window.
Sprite windows:
- If the target window has a sprite layer, the PNG is treated as a sprite import.
- If you have selected sprites, PPUX imports into those sprites in selection order.
- If no sprites are selected, PPUX imports into the layer's sprites from first to last.
- The PNG must use at most 4 total colors including transparency, or at most 3 non-transparent colors.
- The PNG dimensions must align to the current sprite mode:
8x8sprites require multiples of8x8, and8x16sprites require multiples of8x16. - The image is split into sprite-sized frames from left to right, top to bottom.
- Fully transparent frames are skipped.
- When importing into an unselected sprite layer, PPUX also repositions sprites to match the frame grid automatically.
PPU Frame windows:
- Dropping a PNG on a
ppu_framewindow runs the nametable unscramble/import flow for that screen (it matches the PNG against the current patterns in CHR/ROM window and tries to automatically build the actual nametable layout)
CHR and ROM bank windows:
- Dropping a PNG on a CHR-like source window imports the image into the selected tile position, or the top-left if nothing is selected.
Notes:
- PNG drops edit the project/app state and are written out when you save, just like normal tile or sprite pixel edits.
- If the PNG does not meet the color or size rules, PPUX shows a status message explaining why it was rejected.
To build a packaged Windows app from Windows, run:
scripts\build_windows.batThe packaged Windows app will be created only as build\PPUX-<version>-win64.zip.
To build a packaged Linux app from Linux, run:
./scripts/build_linux_appimage.shThe packaged Linux app will be created as build/PPUX-<version>-x86_64.AppImage.
You can also build for Windows and macOS from Linux using ./scripts/build_all.sh (macOS build not tested yet).
The DB lets PPUX recognize specific ROMs and open a tailored starting workspace automatically.
DB entries are matched by ROM SHA-1 and can define open windows, relevant CHR banks, palette windows, ROM-backed views, and the initial workspace arrangement. If no DB entry exists, PPUX falls back to a default layout. User projects (*.lua and *.ppux) take priority over DB defaults.
Current list of games
-
Contra (Japan) is in progress - About 20%
-
Kirby's Adventure (USA) (Rev 1) - Not started yet
-
The Guardian Legend (USA) - Not started yet
NOTE: This is currenly in beta so please be patient while the list is still being expanded
The DB contribution tracker sheet is a shared place to track which games already have DB coverage, which ones are in progress, pending, etc.
Use it to coordinate contributions, avoid duplicate effort, and leave notes about the current status of a game-specific DB entry.
Lua project files are plain Lua tables returned from <rom>.lua:
return {
kind = "project",
projectVersion = 1,
currentBank = 1,
focusedWindowId = "bank",
edits = {},
windows = {}
}The most important fields are windows and edits. For windows, common fields include kind, id, title, x/y/z, zoom, workspace size, viewport size, scroll position, and layer state.
For edits, the data stores per-bank, per-tile pixel edits applied on top of the source ROM data, using a compact compressed format.
The recommended workflow is to save once from the UI, use the generated project (*.lua or *.ppux) as the template, then create windows, layouts, edits, etc, and keep the project growing as you wish (either for personal use, sharing or even for a new DB entry PR).
Notes:
-
PPUX never overwrites the original ROM. Pixel edits and other byte changes (like patches, palette color changes, etc) are written as
<rom>_edited.nes. -
Project files are saved either as
<rom>.luaand<rom>.ppux. -
*.ppuxfiles are just zlib-compressed versions of Lua project files, useful when you want smaller files or prefer not to keep the project contents easily readable.
Best practice: keep the base ROM, edited ROM, and project files in the same folder.
ppu_frame windows are structured screen views. They usually contain one tile layer backed by a compressed nametable stream and, optionally, one sprite overlay layer.
Example:
{
kind = "ppu_frame",
id = "ppu_01",
layers = {
[1] = {
kind = "tile",
bank = 9,
nametableEndAddr = 0x01329B,
nametableStartAddr = 0x013110,
paletteData = { winId = "rom_palette_01" }
},
[2] = {
kind = "sprite",
mode = "8x16",
items = {
{ startAddr = 0x009F2B, bank = 4, tile = 238 },
...
}
},
...
}
}In tile layers, nametableStartAddr and nametableEndAddr define the ROM byte range used for the nametable data handled by that window (it's the same bytes read by an emulator when loading a specific nametable). The app reads from that range when loading the screen data, and writes back into the same range when saving changes.
For sprite layers, startAddr is the most important field because it links the item to the 4 OAM bytes in ROM. The app uses byte 1 for Y position, byte 3 for attributes/palette/mirroring, and byte 4 for X position directly through the app UI. Byte 2 is the exception: in real hardware or emulators, its tile value is interpreted in PPU/VRAM space, not as a direct ROM-bank tile reference. Since the app does not know the final runtime VRAM page layout, bank and tile must also be specified explicitly so the correct source graphics can be resolved in the editor context.
PPU Frame tile layers support noOverflowSupported = true. This means the compressed nametable stream should stay within its original ROM byte budget.
Why it matters: some games leave safe free space after the stream, and some do not.
TMNT II is a good example of this: compressed byte ranges are packed tightly, so PPUX reads one nametable from a defined range while the next nametable begins immediately after it:
Contra (J) example, where the byte "buffer" has plenty of space:
PPUX warns when the compressed stream goes over budget and clears the warning if it returns to a valid size.
PPUX currently includes one nametable codec implementation aimed at Konami-style streams (konami.lua). New codecs for different games/styles will be added as the app development progresses.
oam_animation windows are ROM-backed sprite animation windows where each layer is effectively one frame.
Example:
{
kind = "oam_animation",
id = "oam_animation_01",
layers = {
[1] = {
kind = "sprite",
mode = "8x16",
items = {
{ startAddr = 0x0095FA, bank = 1, tile = 256 },
...
}
},
...
}
}Important fields are frame timing (delaysPerLayer), sprite frames (layers), local origin, palette source, and ROM-backed startAddr entries.
rom_palette windows are 4x4 palette editors backed directly by ROM addresses.
Example:
{
kind = "rom_palette",
paletteData = {
romColors = {
[1] = { 0x01F688, 0x0112ED, 0x0112EE, 0x0112EF },
[2] = { 0x01F688, 0x0112F0, 0x0112F1, 0x0112F2 },
[3] = { 0x01F688, 0x0112ED, 0x0112EE, 0x011243 },
[4] = { 0x01F688, 0x0112F0, 0x0112F1, 0x011252 },
}
}
}So each romColors[row][col] stores a ROM address for a given palette color. The first column is the universal background color, usually one single "shared" ROM address.
Some windows refer to other windows by id, for example:
paletteData = {
winId = "rom_palette_02"
}The referenced window must exist elsewhere in the same windows array and will be used as the palette source.
PPUX can apply small ROM patches from project data before windows are built (so the user is already working on top of "patched" ROM).
This is meant for targeted graphics-related setup such as forcing a game state or changing a small byte sequence. It is not a replacement for a full ROM hacking workflow.
PPUX includes a unit test suite. See Unit Testing.
PPUX also includes visible end-to-end test scenarios that boot the real app. See E2E Testing.
Detailed video tutorials are planned.
Dev notes:
- Create a shader google sheet so people can add themselves and indicate which game(s) they'll add to the DB (DONE)
- Add Tiles mode for windows (similar to "Tiling" windows managers on Linux)
- Add more e2e tests (DONE, needs more tho)
- Add a compressed version of the lua project, maybe *.ppux (DONE)
- Separate DB into per-game Lua files (DONE)
- Add a "recently loaded" list in main menu (DONE)
- Re-structure main menu into sub-menus (DONE)
- Add contextual menus for windows and empty space (DONE-ish)
- Standarize modals to use panel as a base, and make panels more robust/flexible (DONE)
- Fix issue: project loading way to many items when project was saved with CHR sync ON (FIXED)
- Be able to create and populate/configure OAM/ROM-backed windows thru the UI (big task, needs split)














