Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .conductor/settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ icon = "gamepad-2"
# anything else, run the headless mode by hand; see CLAUDE.md. What it is worth is proving the
# emulator works where the button above is greyed out.
[scripts.run.headless]
command = "mvn -q compile exec:exec@headless -Dmynes.args=\"--rom src/test/resources/nestest/nestest.nes --frames 300 --screenshot last --expect-not-blank\""
command = "mvn -q compile exec:exec@headless -Dmynes.args=\"--rom mynes-core/src/test/resources/nestest/nestest.nes --frames 300 --screenshot last --expect-not-blank\""
available_in = [ "local", "cloud" ]
icon = "terminal"
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:

# Named without a path so that `sha256sum -c SHA256SUMS` works next to the downloaded zip.
- name: Checksum
working-directory: target
working-directory: mynes-desktop/target
run: |
sha256sum mynes-*.zip > SHA256SUMS
cat SHA256SUMS
Expand All @@ -61,5 +61,5 @@ jobs:
run: |
gh release create "$GITHUB_REF_NAME" \
--generate-notes \
target/mynes-*.zip \
target/SHA256SUMS
mynes-desktop/target/mynes-*.zip \
mynes-desktop/target/SHA256SUMS
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ jobs:

# ~1.4GB of Tom Harte fixtures. The committed subset covers every opcode without
# this, so a cache miss that fails to download is not fatal to the test run.
#
# Inside the module because HarteCaseLoader resolves the path relative to wherever
# Surefire runs, which is the module rather than the checkout.
- name: Cache 6502 test data
uses: actions/cache@v6
with:
path: testdata/nes6502
path: mynes-core/testdata/nes6502
key: nes6502-${{ hashFiles('scripts/download-6502-tests.sh') }}

- name: Download 6502 test data
Expand Down
73 changes: 54 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Working on MyNES

A NES emulator in Java 25, built with Maven. `mvn -B test` runs everything.
A NES emulator in Java 25, built with Maven. Three modules -- `mynes-core`, `mynes-headless`,
`mynes-desktop` -- and `mvn -B test` at the root still runs everything.

## Seeing what the emulator does

Expand All @@ -12,16 +13,22 @@ Build the jar once, then run it as often as you like:

```sh
mvn -B package -DskipTests
JAR=target/mynes.jar
JAR=mynes-desktop/target/mynes.jar

java -jar $JAR --headless --rom ROM.nes --frames 900 \
--input 60/40x3:start --screenshot 300,last --audio --dump ram
```

The jar is under `mynes-desktop/` because that is the module with a main class in it, but it is
still one file and still the whole emulator: the fat jar flattens all three modules and their
dependencies into it.

`java -jar $JAR --headless --help` lists every option. Maven can run it too --
`mvn -q compile exec:exec@headless -Dmynes.args="--rom ROM.nes --frames 300"` -- but it costs a
couple of seconds a run against the jar's third of one, so build the jar for anything iterative.
`mvn -q compile exec:exec` opens the window, which a cloud workspace has nowhere to put.
`mvn -q compile exec:exec` opens the window, which a cloud workspace has nowhere to put. Neither
needs a `-pl`: the modules that have no main class declare the goal skipped, and it runs from the
root of the checkout, so a `--rom` path means what it looks like.

### What you get, and where

Expand Down Expand Up @@ -151,10 +158,10 @@ so `run.state.startedFromPowerOn` in the report is part of what to check before

## What gets released

`mvn package` also writes `target/mynes-<version>.zip` -- the jar, a launcher for each kind of shell,
and the licences. `scripts/smoke-distribution.sh` unpacks it and runs a cartridge out of it, and both
workflows call that, so a distribution somebody has broken fails on the pull request that broke it
rather than at the moment a tag is pushed.
`mvn package` also writes `mynes-desktop/target/mynes-<version>.zip` -- the jar, a launcher for each
kind of shell, and the licences. `scripts/smoke-distribution.sh` unpacks it and runs a cartridge out
of it, and both workflows call that, so a distribution somebody has broken fails on the pull request
that broke it rather than at the moment a tag is pushed.

Two things in there are counted rather than derived, and have to move when what they count does. The
smoke test expects **12 palettes** -- `Palettes.NESDEV` plus the eleven under `/palettes` -- because a
Expand All @@ -163,7 +170,15 @@ build would notice one going missing. And `THIRD-PARTY.md` names the two librari
carries, which is the file to write in if a third ever earns its place.

Releasing is a tag and nothing else. `.github/workflows/release.yml` refuses one whose name disagrees
with the pom, so the version in `pom.xml` moves first and `git tag v<version>` follows it.
with the pom, so the version moves first and `git tag v<version>` follows it. There are four poms to
move it in now, which is a job for the tool rather than for four edits:

```sh
mvn -B versions:set -DnewVersion=0.3.0 -DprocessAllModules -DgenerateBackupPoms=false
```

The workflow's check reads the root pom, and `help:evaluate` is an aggregator goal, so it still
answers with one version rather than three.

## House style

Expand All @@ -187,20 +202,40 @@ The code has a strong voice. Match it rather than the language's defaults.

## Layout

Three Maven modules, and the arrows between them only point one way.

```
mynes/ the console: CPU, PPU, APU, BUS, MMU, VRAM, Cart, Region, controllers
mynes/mappers/ mappers 0 to 4
mynes/state/ save states and battery .sav files
mynes/debug/ the disassembler and the breakpoints, shared by the window and the REPL
mynes/video/ colour indices to pixels: the overscan crop and the frame renderer
mynes/headless/ the command line mode
mynes/ui/ the Swing window, the palettes, the key bindings, the CHR viewer, the debugger
mynes-core/ depends on nothing
mynes/ the console: CPU, PPU, APU, BUS, MMU, VRAM, Cart, Region, controllers
mynes/mappers/ mappers 0 to 4
mynes/state/ save states and battery .sav files
mynes/debug/ the disassembler and the breakpoints, shared by the window and the REPL
mynes/video/ colour indices to pixels: the overscan crop and the frame renderer
mynes/palette/ the measured RGB tables, and the loader that reads them out of /palettes

mynes-headless/ depends on core
mynes/headless/ the command line mode

mynes-desktop/ depends on core and headless; FlatLaf and MigLayout live here
mynes/ui/ the Swing window, Main, the key bindings, the CHR viewer, the debugger
```

The core knows nothing about the front end, and now it *cannot*: `Cart.load` takes a `byte[]`, `NES`
has no UI dependency, `nes.tick()` is the only clock, and the PPU emits colour *indices* -- never
RGB, because which RGB is a question about televisions. What used to be a rule about imports is a
rule about the class path. A chip that wants a window, or a REPL that wants a `JDialog`, does not
compile.

Which makes one check worth running when the dependencies change:

```sh
mvn dependency:tree -pl mynes-core # nothing but the two test artifacts
mvn dependency:tree -pl mynes-headless # no FlatLaf, no MigLayout
```

The core knows nothing about the front end. `Cart.load` takes a `byte[]`, `NES` has no UI
dependency, `nes.tick()` is the only clock, and the PPU emits colour *indices* -- never RGB, because
which RGB is a question about televisions. Keep it that way: nothing in `mynes` or `mynes/headless`
should reach into `mynes/ui`, save for the palette tables.
The palettes are in the core rather than beside the window because both front ends draw with them
and neither owns them. `NESPalette` is 512 packed integers and `Palettes` reads files; the one piece
of Swing in that story, `PaletteDialog`, stayed behind in `mynes/ui/`.

`peek` means "read without side effects", and it is load-bearing. `VRAM.read` tells the mapper what
address is on the bus, and MMC3 counts those to drive its scanline interrupt -- so a debugger that
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ That opens the window. Or build a jar once and run that:

```sh
mvn -B package -DskipTests
java -jar target/mynes.jar
java -jar mynes-desktop/target/mynes.jar
```

The same `mvn package` also writes the release zip into `target/`, so what the releases page carries
is never anything a build here has not already made.
One jar, whichever way: it is three Maven modules -- `mynes-core` for the console, `mynes-headless`
for the command line, `mynes-desktop` for the window -- flattened into one file with its
dependencies. The core depends on nothing at all, which is the point of it being separate.

The same `mvn package` also writes the release zip into `mynes-desktop/target/`, so what the releases
page carries is never anything a build here has not already made.

## Controls

Expand Down Expand Up @@ -224,7 +228,7 @@ on a machine with no display, or for a coding agent that cannot look at a window

```sh
mvn -B package -DskipTests
java -jar target/mynes.jar --headless \
java -jar mynes-desktop/target/mynes.jar --headless \
--rom smb.nes --frames 900 --input 60/40x3:start --screenshot 300,last --audio
```

Expand Down Expand Up @@ -285,7 +289,7 @@ the exact per-cycle bus traffic. nestest passes as well.
For the full 10,000 cases per opcode, fetch the upstream set once:

```sh
./scripts/download-6502-tests.sh # ~1.4GB into the gitignored testdata/
./scripts/download-6502-tests.sh # ~1.4GB into the gitignored mynes-core/testdata/
mvn test # picks the full set up automatically
```

Expand All @@ -296,8 +300,8 @@ mvn test -Dgroups=bus-trace
mvn test -DexcludedGroups=bus-trace
```

**The PPU** runs blargg's test ROMs, vendored under `src/test/resources` together with the readme
that came with each suite: `ppu-vbl-nmi` (VBlank and NMI timing to a single PPU clock),
**The PPU** runs blargg's test ROMs, vendored under `mynes-core/src/test/resources` together with
the readme that came with each suite: `ppu-vbl-nmi` (VBlank and NMI timing to a single PPU clock),
`ppu-sprite-hit`, `ppu-sprite-overflow`, `oam` (`oam_read` and `oam_stress`), `ppu-open-bus`,
`ppu-read-buffer` and `ppu-tests-2005` (palette RAM, VRAM access, sprite RAM, VBlank clear time).

Expand All @@ -324,10 +328,10 @@ contradicts `5-MMC3` by design. No real chip passes both.

**The whole console at once** runs 100thCoin's
[AccuracyCoin](https://github.com/100thCoin/AccuracyCoin), one NROM cartridge carrying 141 scored
tests and vendored under `src/test/resources` like the rest. With the cursor on a page header, Start
runs every one of them and draws the table below: a column per page of the menu and a tile per test,
red with an error code on it where one failed, and a pale number where the ROM accepts more than one
answer and that is which one came back. 134 pass.
tests and vendored under `mynes-core/src/test/resources` like the rest. With the cursor on a page
header, Start runs every one of them and draws the table below: a column per page of the menu and a
tile per test, red with an error code on it where one failed, and a pale number where the ROM accepts
more than one answer and that is which one came back. 134 pass.

![AccuracyCoin's results table](shots/accuracycoin.png)

Expand Down
3 changes: 2 additions & 1 deletion THIRD-PARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ writes its own reports rather than borrowing it.

The palettes bundled under `/palettes` in the jar are data rather than software, and are neither
FlatLaf's nor MigLayout's. Their sources, terms and credits are in the `PROVENANCE` file beside them,
which is inside `mynes.jar` and also in the repository at `src/main/resources/palettes/PROVENANCE`.
which is inside `mynes.jar` and also in the repository at
`mynes-core/src/main/resources/palettes/PROVENANCE`.
28 changes: 28 additions & 0 deletions mynes-core/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.github.dimiro1</groupId>
<artifactId>mynes</artifactId>
<version>0.2.0</version>
</parent>

<artifactId>mynes-core</artifactId>
<packaging>jar</packaging>

<name>mynes-core</name>
<description>The console: CPU, PPU, APU, the mappers, save states, and the picture it makes.</description>

<dependencies>
<!-- cpu.HarteCase and cpu.HarteCaseLoader read the Tom Harte SingleStepTests fixtures with
it. Nothing in src/main touches it, and nothing here has a compile dependency at all. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -1458,7 +1458,7 @@ private int backgroundPixel(final int x) {
* Both belong here rather than in the front end, because the hardware really does force the
* index down and really does put those three bits on the wire. What the resulting signal looks
* like on a television is somebody else's problem -- see
* {@code com.github.dimiro1.mynes.ui.palette.NESPalette}.
* {@code com.github.dimiro1.mynes.palette.NESPalette}.
*
* @return {@code emphasis << 6 | entry}, 0 to 511.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.dimiro1.mynes.ui.palette;
package com.github.dimiro1.mynes.palette;

/**
* One measurement of the colours a 2C02 produces, in a shape the front end can draw with.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.dimiro1.mynes.ui.palette;
package com.github.dimiro1.mynes.palette;

import com.github.dimiro1.mynes.Region;

Expand All @@ -17,7 +17,9 @@
* goes for a missing resource.
*/
public final class Palettes {
private static final Logger logger = System.getLogger("UI");
// "PALETTE" rather than "UI": both front ends load these, and the window is no longer the only
// thing that can be reading a palette file when one of them turns out to be unreadable.
private static final Logger logger = System.getLogger("PALETTE");

/**
* The NESdev set, and the one MyNES has always drawn with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* and not a common superclass.
* <p>
* The palette arrives as a plain {@code int[512]} rather than as a
* {@link com.github.dimiro1.mynes.ui.palette.NESPalette}, so that drawing a picture does not oblige
* {@link com.github.dimiro1.mynes.palette.NESPalette}, so that drawing a picture does not oblige
* a caller to know what a palette is made of, and so that this package depends on nothing but the
* PPU and {@code java.awt.image}.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.dimiro1.mynes.ui.palette;
package com.github.dimiro1.mynes.palette;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.dimiro1.mynes.ui.palette;
package com.github.dimiro1.mynes.palette;

import com.github.dimiro1.mynes.Region;
import org.junit.jupiter.api.Test;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.github.dimiro1.mynes.video;

import com.github.dimiro1.mynes.PPU;
import com.github.dimiro1.mynes.ui.palette.Palettes;
import com.github.dimiro1.mynes.palette.Palettes;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
Expand Down
Loading