Compare commits

...
466 Commits
Author SHA1 Message Date
Luke Usher dd73766522 ps1: implement GTE command timing 2026-11-06 21:32:31 +00:00
Luke Usher d3b8a29910 ps1: fix breakpoint and address-error exceptions 2026-11-06 21:31:44 +00:00
Luke Usher 05232171d4 ps1: fix spu envelopes and cd audio capture
Fix envelope timing and key-on delay, jump to the repeat address on sample end.
Capture CD audio before volume processing,
Fix sign handling for cd-rom volume and pitch modulation

ref: https://psx-spx.consoledev.net/soundprocessingunitspu/
2026-11-04 19:53:25 +00:00
Luke Usher 15ef1ac2d5 ps1: overhaul sio timing and fifo behavoir
Devices are now in charge of their own timing, fix Memory Card and Controller timing to be closer to hardware.

Fixes an infinite loop polling for memory cards in Ace Combat 3 (USA)
2026-11-04 19:23:03 +00:00
Luke Usher 234c99911f ps1: gpu write fifo should not be blocked when the gpu is busy drawing 2026-11-03 15:34:12 +00:00
Luke Usher 94e66350a1 ps1: sample external bus reads after wait states apply 2026-11-03 13:34:13 +00:00
Luke Usher c4f133ac0b ps1: abort pending MDEC decode on reset 2026-11-03 11:19:51 +00:00
Luke Usher 4307bf2eff ps1: fix motorOn command
it was responding but not actually turning the motor flag on.
2026-11-02 17:05:17 +00:00
PixelMechanic0 17f29369c4 n64: avoid marking an empty RDP queue busy
Clearing DPC_FREEZE calls flushCommands even when END equals CURRENT. Previously, this set pipeBusy and startGclk without executing any commands, leaving an initially idle RDP reporting busy.

Return before setting the busy flags when no forward command range is available. Preserve existing busy state for starved pipelines and leave START/END latching unchanged.
2026-11-01 22:40:37 +00:00
Luke Usher dc4f4659d4 neogeo: add option to allow loading MVS-only or AES-only games on the other system 2026-10-31 16:03:58 +00:00
Matthew Callis e9639ebae3 nall: handle partial TCP sends and reconnects 2026-10-30 10:52:35 +00:00
Matthew Callis bdf0b044e8 macos: enable ARC for hiro and ruby with CMake 2026-10-30 10:52:01 +00:00
Matthew Callis 26c56bb4bc desktop-ui: preserve window placement on resolution changes 2026-10-30 10:51:33 +00:00
Luke Usher 9c5ed3f68d hiro: fix tab frame races on gtk 2026-10-29 16:02:01 +00:00
Luke Usher b6890c9eff ci: use switch freebsd cmake pkg to cmake-core 2026-10-29 14:54:19 +00:00
Luke Usher 035fb15c71 tools: replace native mame2bml tool with Python scripts
The following are new behavoirs in the python tooling:

Extract DIP switch definitions, input mappings, and other metadata by parsing tables directly from the MAME source tree; this lifts metadata (such as dip switch names and bit-values and button labels) into Arcade.bml, no code is lifted.
Support custom overrides for information that is missing or insufficient in MAME's database.
Allow ROMs to be excluded from the generated database, including incomplete dumps, unsupported bootlegs, and non-game software.
2026-10-29 10:12:17 +00:00
Luke Usher 6fd11e291f arcade: add Sega System E
This is essentially just a master system with two VDPs and an optional encrypted z80
2026-10-29 10:12:17 +00:00
Luke Usher 1c07ebf70c desktop-ui: add per-game input overrides
Extend the existing per-system input override functionality to support game-specific controller mappings.

The intended workflow is for users to configure their Virtual Pad once. The Virtual Pad attempts to map inputs to each system's controller based on the physical position of its buttons.

Users can then customize mappings by adding overrides for specific inputs per-system, or per-game. Per-game overrides can only be configured while the relevant game is loaded.

ares attempts to strip common Disc, Disk, Side and Part suffixes from game titles, allowing multi-disc games to share the same input configuration.
2026-10-29 10:12:17 +00:00
Luke Usher 569acf7ba1 arcade: improve neo geo, centralize dip-switch handling and add support for sg1000a encryption
Introduce a shared DIP switch implementation for arcade systems, replacing system-specific implementations.

Neo Geo:

Implement the NEC uPD4990A real-time clock.
Implement the MVS hardware watchdog.
Fix IRQ acknowledgement register handling to that only pending IRQs can be acknowledged
Move ROM decryption and protection handling into runtime PCB emulation, more closely reflecting the behaviour of original hardware.
Add support for additional protection schemes and game-specific memory mappers.

SG-1000A:

Add support for the encrypted Z80 variant used by Championship Wrestling.
Introduce separate opcode fetch and data read paths for the Z80, allowing accurate emulation of hardware-dependent decryption.

Portions of the ROM decryption and protection implementations are derived from MAME. Only code distributed under permissive licences, including the BSD 3-Clause License, has been incorporated.

All MAME-derived implementations are explicitly identified in the source code, with the applicable copyright notices and licence terms preserved in the project's LICENSE file.
2026-10-29 09:49:27 +00:00
Luke Usher 47bab688e6 a26: fix crash with unusual display co-ordinates 2026-10-28 19:01:54 +00:00
remutro 460fec09a0 Fix layout on Cores settings UI 2026-10-27 22:43:31 +00:00
Luke Usher 5d093ad9e6 ps1: run textured draws in parallel to the cpu
To measure texture cache timing, textured draws previously waited for
the renderer to catch up before emulation could continue.

Now we wait only when the GPU becomes ready for another draw.

draw timing observed by the game is unaffected.
2026-10-27 09:00:05 +00:00
Luke Usher 3474c5c1bc a52: improve ANTIC playfield DMA timing
Moon Patrol's HSCROL write was landing too early in ares, corrupting the mountains

Altirra Hardware Reference Manual was used to determine correct behavoir, which I believe to be trustworthy as the introduction specifies that "This document attempts to describe the hardware in detail as the target to which the emulator aspires to imitate"
2026-10-27 10:57:05 +00:00
Luke Usher d400eee976 ps1: toggling 'overscan' should not change aspect ratio 2026-10-26 21:52:46 +00:00
Luke Usher 8128704f23 screen: add configurable deinterlacing modes
Weave (default): combines the current field with the previous field. this preserves detail but may introduce combing during motion. This is how ares behaved historically.

Bob: displays the current field and interpolates missing lines. This prevents motion combing but causes a noticable loss of vertical detail and may flicker thin lines.

Blend: displays an average of consecutive bobbed fields. Behaves similar to bob, but swaps flicker for motion blur.

Phosphor: displays only the currently displaying field while simulating phosphor decay. This Produces a darker image with visible scanlines and may flicker, but is closer to how a real CRT handles interlaced video.
2026-10-26 16:41:29 +00:00
Luke Usher a774671dc0 ps1: take texture cache into account when calculating GPU draw timing
nocash DIAG's hardware cache tests measure miss penalties of ~60 CPU
clocks for eight entries and ~120 clocks for sixteen entries. This is
approximately 12 GPU clocks per 8-byte refill.

We use 13 clocks here as it better fits JaCzekanski's GPU bandwidth
test; textured drawing timings now measure within ~1-2% of the hardware
baseline.
2026-10-26 11:40:08 +00:00
Luke Usher 3f268697d2 ps1: avoid overflow on cd-audio/xa-adpcm mixing 2026-10-26 09:44:56 +00:00
Luke Usher 26382de232 ps1: various bug-fixes/feedback amendments
- prevent discarded polygons from counting in draw timing calculations
- fix depth mode changes applying one frame too late
- fix cd-rom stop command
- prevent MDEC output fifo from losing data when full and reduce output size to a more realistic 32 words-
2026-10-25 22:15:09 +00:00
Luke Usher 9c65426aab ci: use clang-cl for ares-windows-x64, move mingw-clang64 to build-aux
Rationale is that the clang64 ci is resulting in a build that drops as low as 40vps in FMV sequences in the PS1 core, vs 90vps in the same scene for clang-cl

Pinning a known-performant version of clang was considered (local testing proved clang22 produces more performant builds than clang20), but this is not viable with how msys2 is managed. Investigation concluded that we cannot reliably update the clang msys environment; just running 'update:true' breaks the build due to a ymfm issue with the clang23 runtime, and installing the 'clang22' packages still uses the clang23 runtimes, it only pins the compiler itself.
2026-10-25 10:06:49 +00:00
Christopher Bonhage 4355718dda n64: commit the PC at the end of every JIT block
A block can end before an instruction that has a gdb breakpoint. Its
last instruction can then be straight-line code, which does not commit
the pipeline PC. The block exited with ipu.pc at the last commit point,
so the CPU ran the instructions after that point a second time. If that
point was an internal entry of the same block, the CPU looped forever
and the breakpoint never hit.

Commit the pipeline PC on the last instruction of every block. All other
block ends already commit there, so code without breakpoints is
unchanged.
2026-10-25 07:30:52 +00:00
Luke Usher 022805be81 ps1: overhaul timing and hardware accuracy
- more accurately model the CPU bus, write buffering and cache behavior
- improve DMA arbitration, chopping, transfer timing, and interaction with CPU
- rework CD-ROM command timing, deferred responses, interrupts, sector delivery, subchannel data, and disc session handling
- implement the GPU command FIFO, busy timing, texture caches, rasterization, and improve scanout accuracy
- improve timer synchronization, reset, interrupt, and blanking behavior
- improve peripheral, SPU, MDEC, and memory-mapped I/O timing
- add support for Datel Action Replay and Blaze Xplorer cartridges
- add support for the Pixel Accuracy toggle to enable per-pixel scanout (required by tajmiing demo)

These changes were validated against psxtest_cpx, psxtest_gpu, nocash diag.exe, JaCzekanski's tests, and the pcsx-redux CD-ROM torture tests.
Most of tests now pass, and known problematic games such as Monkey Hero are working.

Framebuffer output matches expected hardware output pixel-for-pixel in the nocash diagnostics and psxtest_gpu primitive tests.

Timer test results generally remain within a small margin of hardware baseline results (when compared against the psx.log reference point shipped with the tests)

psxtest_cpx and psxtest_gpu do not initialize controllers correctly when launched directly from the BIOS. You must run them them through software that initializes the hardware first, such as a Datel Action Replay or Blaze Xplorer cartridge. These devices can be configured in Core settings. This issue exists on real hardware and is correct behavoir.

ares can now run the tajmiing demo and all its effects end-to-end.

tajmiing changes display registers at individual-pixel boundaries, so it requires the Pixel Accuracy option in Video settings. Scanline-level synchronization remains the default as oridnary software does not depend on mid-scanline register changes.
2026-10-24 16:48:25 +01:00
Matthew Callis c76aaffa6e sfc: latch window edges once per scanline
Latch the left and right edge matches independently until the next scanline, preserving the inclusive right edge. This prevents mid-line bounds writes from reopening a stopped window, as demonstrated by Mickey Magic and MiSTer SNES PR #508.

Serialize the latches and bump the SNES state format. Fixes ares-emulator/ares#2645.
2026-10-22 23:02:05 +01:00
Reaper 9408cb43d4 desktop-ui: force 3-button mode for incompatible Mega Drive games 2026-10-06 15:03:27 +01:00
Reaper 4565c732bd desktop-ui: persist controller selection 2026-10-06 12:43:10 +01:00
Stefan Schlosser a776c509bd CI: Update FreeBSD runner to 15.1
With FreeBSD 15.0 going EOL very soon, it is time to update the runner
in the CI to FreeBSD 15.1. This version is supported by Cross-Platform
GitHub Actions since version 1.3.0, and the runner is updated to the
currently newest version 1.6.0, which corresponds to the commit
https://github.com/cross-platform-actions/action/commit/e0b9770014ba65d5e0815f15b74031c3a635f641.

Using the operating system's shell for running commands inside steps is
deprecated. Cross-Platform GitHub Actions provides and recommends the
new custom shell cpa.sh instead, which the runner is now using.

This requires to split the "Build: FreeBSD" step into at least one
separate step for setting up the VM before executing commands in the
following steps. This opens up the opportunity to be more in line with
the Linux runner by installing the build dependencies in its own step,
"Install FreeBSD Dependencies".

Before installing dependencies, the runner now updates the preinstalled
packages in the VM via pkg upgrade -y. This ensures that the whole VM is
cohesively up-to-date.

Last but not least, the TARGET_PRESET environment variable must be set
in the "Build: FreeBSD" step with the --environment-variables option of
cpa.sh, or else it won't be propagated to the build environment.
2026-10-04 23:35:10 +01:00
Akiteru 2976a1ab6b sfc: CX4 accuracy improvements
Ported directly from bsnes PRs 377 and 401.
- When triggering a cache preload by writing to $7f48, update the pb register with the prepared program page
- When preloading a page, ignore all runtime checks and reload the page immediately
- Step 1+WS on both reads for cache, not just one
- Remove extra cycle on bus reads/writes
- Remove extra cycle on DMA to internal RAM (fixes excessive slowdown in Morph Moth fight)
2026-10-04 23:34:57 +01:00
Matthew Callis 4cb8d92b44 sfc: add Super Multitap support 2026-09-23 10:42:53 +01:00
user 17813a3ccd gba: CPU memory access fixes 2026-09-15 09:54:32 +01:00
AeroModes 9c9e4148b0 mia: relax FDS BIOS verification checks 2026-09-14 22:38:54 +01:00
Luis Felipe Strano Moraes b5f2943acb md: add kmod message log channel 2026-09-14 22:38:35 +01:00
ZengGengSen a137e844ac eeprom: implement X24C01 four-byte page writes 2026-09-14 22:37:33 +01:00
ZengGengSen b71dabe7a9 fc: reuse shared X24C01 for Datach EEPROM 2026-09-14 22:37:33 +01:00
ZengGengSen e40b0db1ed fc: implement Datach external EEPROM 2026-09-14 22:37:33 +01:00
Aitor Guevara 1e882cdd12 hiro: ensure focus when launched from a frontend in macos
On macoS, a binary executed directly by another process (e.g. a frontend
spawning and emulator) starts as an unregistered background process. It
must be promoted to a regular GUI application to reclaim focus.
2026-09-13 16:18:40 +01:00
AeroModes 09d0c939c5 fc: initialize VRC7 YM2413 patch set at power-on 2026-09-13 16:11:56 +01:00
AeroModes 412c554e9c fc: honor VRC7 WRAM enable bit for reads and writes 2026-09-13 16:11:48 +01:00
AeroModes fd12c9174e fc: support VRC7 submapper pinouts 2026-09-13 16:11:41 +01:00
zoncabe b7cef2049b n64: read GameCube triggers through a Trigger node 2026-09-13 16:03:38 +01:00
zoncabe 0f6b50412b n64: clarify comment on opposing d-pad directions 2026-09-13 16:03:38 +01:00
zoncabe 6d6e1037e8 n64: add GameCube controller support 2026-09-13 16:03:38 +01:00
Matthew Callis b15d4d378c gdb: continue polling after processing a message 2026-09-13 15:55:10 +01:00
Matthew Callis 2bd312e602 Fix macOS build when show-sdk-platform-version fails 2026-09-13 15:51:16 +01:00
Giovanni Bajo af4cbb04f0 Align semantic of all RTC implementations:
* Initialize using the PC localtime
* Handle backward time warp by ignoring it
2026-09-06 12:34:34 +01:00
Giovanni Bajo dee49f55ae n64: avoid infinite loop at boot when time goes backward 2026-09-06 12:34:34 +01:00
Giovanni Bajo e6130f66e7 n64: speed up first boot of games with RTC
There is no reason to advance ~1.7 billion times from the epoch. We can
just initialize the RTC with the current time.
2026-09-06 12:34:34 +01:00
user 5de16c334d fc: implement PPU internal shift registers 2026-09-06 12:33:37 +01:00
ZengGengSen 7b51c8ab71 desktop-ui: guard hot-swappable port changes
Generic port menu callbacks could mutate scheduler threads and audio streams while the emulator worker was active. Route both disconnect and connect actions through one Program::Guard-protected helper and add a deterministic concurrent regression test.
2026-08-30 19:24:24 +01:00
ZengGengSen 66a53e2ed8 a26: advance save-state format to v141 2026-08-30 19:24:24 +01:00
ZengGengSen 24ab7df1e4 a26: add QuadTari controller support 2026-08-30 19:24:24 +01:00
ZengGengSen 8381b5ae05 a26: add AtariVox speech synthesis 2026-08-30 19:24:24 +01:00
ZengGengSen 0b87e304a0 a26: add SaveKey controller support 2026-08-30 19:24:24 +01:00
ZengGengSen 9092f818b9 a26: add KidVid Voice Module support 2026-08-30 19:24:24 +01:00
ZengGengSen a6869c924d a26: add MindLink controller support 2026-08-30 19:24:24 +01:00
ZengGengSen a144d3150a a26: add XG-1 light gun support 2026-08-30 19:24:24 +01:00
ZengGengSen 5ca93ea4d2 a26: add relative pointing controllers 2026-08-30 19:24:24 +01:00
ZengGengSen cc6b83228f a26: add extra-button controller support 2026-08-30 19:24:24 +01:00
ZengGengSen 4a71d93bb6 a26: add ROM-profiled phosphor persistence 2026-08-30 19:24:24 +01:00
ZengGengSen 751f615205 mia: add Atari 2600 cartridge database 2026-08-30 19:24:24 +01:00
ZengGengSen 39ca282454 a26: implement MovieCart streaming cartridge 2026-08-30 19:24:24 +01:00
ZengGengSen f0c946b55e a26: implement ELF executable cartridge 2026-08-30 19:24:24 +01:00
ZengGengSen 742cc93106 armv6m: add Cortex-M0 processor component 2026-08-30 19:24:24 +01:00
ZengGengSen 6495f9ea0f a26: implement FA2 cartridge 2026-08-30 19:24:24 +01:00
ZengGengSen de92243f40 a26: implement EFF cartridge 2026-08-30 19:24:24 +01:00
ZengGengSen 403c81a8cb a26: implement Chetiry cartridge 2026-08-30 19:24:24 +01:00
ZengGengSen b255ec7f31 a26: add persistent cartridge storage 2026-08-30 19:24:24 +01:00
ZengGengSen 22af611ad5 a26: implement BUS cartridges 2026-08-30 19:24:24 +01:00
ZengGengSen 73d1382c61 a26: implement CDF cartridges 2026-08-30 19:24:24 +01:00
ZengGengSen 7f40b23068 a26: implement DPC+ cartridge 2026-08-30 19:24:24 +01:00
ZengGengSen 99f2d442a7 a26: add Harmony cartridge runtime 2026-08-30 19:24:24 +01:00
ZengGengSen 6c629246bc a26: implement ordinary homebrew cartridge mappers 2026-08-30 19:24:24 +01:00
ZengGengSen bd4e506a19 a26: normalize supported cartridge dump layouts 2026-08-30 19:24:24 +01:00
ZengGengSen 0f15baebfd a26: implement DPC cartridge 2026-08-30 19:24:24 +01:00
ZengGengSen 6cf7770c4c a26: implement ordinary cartridge mappers 2026-08-30 19:24:24 +01:00
ZengGengSen cd3cc0087e a26: finalize cartridge board interface 2026-08-30 19:24:24 +01:00
ZengGengSen 5578e7ca89 a26: improve RIOT accuracy 2026-08-30 19:24:24 +01:00
ZengGengSen eac791413a a26: improve TIA and video accuracy 2026-08-30 19:24:24 +01:00
ZengGengSen 65e69f7159 a26: implement controller devices 2026-08-30 19:24:24 +01:00
ZengGengSen fb7544b169 a26: improve CPU bus timing 2026-08-30 19:24:24 +01:00
ZengGengSen a2d94ebba1 a26: initialize CPU interrupt tracer 2026-08-30 19:24:24 +01:00
Giovanni Bajo e4217366cf n64: report the up-to-date cycle counter when software reads COUNT
The CPU keeps a free-running cycle counter (COUNT) that guest software
reads to measure elapsed time. Internally the emulator does not update
that counter after every instruction: cycles are accumulated in a
running clock and only folded into COUNT when the CPU synchronizes with
the rest of the machine. Reading the register returned the folded value
only, ignoring any cycles accumulated since the last fold, so the value
handed back to software could be stale.

How stale depends on how often synchronization happens, which is why
this was mostly invisible with the interpreter and clearly wrong with
the recompiler. The interpreter synchronizes after every instruction, so
at most one instruction worth of cycles was missing. The recompiler
translates a whole run of instructions into a single unit of host code
and only folds the accumulated cycles at its boundaries, so several
consecutive reads of COUNT inside one such unit all returned the exact
same number. Code that reads the counter repeatedly to check that time
is moving forward would see it standing still; the test_timer_ticks test
in the libdragon test suite fails this way, with two successive readings
comparing equal.

Track how many cycles have already been folded into COUNT in a separate
countClock field, so the cycles accumulated since then can be recovered
at any point. Reading COUNT now folds those pending cycles in first,
which both returns an up-to-date value and evaluates the COMPARE match
at the same moment, keeping the register and the timer interrupt
consistent with each other. Writing COUNT or COMPARE folds them in as
well, so the pending cycles are attributed to the old value rather than
being added on top of the newly written one.
2026-08-26 23:38:43 +01:00
Giovanni Bajo 430def86f2 n64: fix timer interrupt never firing when COMPARE is zero
The CPU has a free-running cycle counter (COUNT) and a programmable
target register (COMPARE); an interrupt is raised whenever the counter
reaches the target. The check for that event was written as
"count < compare && count + elapsed >= compare", which assumes the
target always lies ahead of the current counter value in plain
arithmetic order. Since both registers are unsigned, that assumption
breaks as soon as the target is behind the counter, and it breaks
permanently when the target is exactly zero: no unsigned value is ever
less than zero, so the first half of the condition is never true and the
interrupt is never raised at all.

That is not a corner case in practice. The counter is only 32 bits wide
and wraps around roughly every 90 seconds, so software that wants to
maintain a wider timestamp typically arms the target at zero in order to
be notified on every wrap. libdragon did exactly this: timer_init()
programs COMPARE to zero and used the resulting interrupt to increment
the high half of a 64-bit tick counter. Under ares that interrupt never
arrived, so the high half was never incremented and timer_ticks() went
backwards across a wrap, which is what the test_timer_ticks test in the
libdragon test suite reports.

Replace the ordered comparison with a modular distance computed over the
counter's full range: the number of ticks remaining until the target is
(compare - count) truncated to the counter width, and the interrupt
fires when at least that many ticks elapse. This behaves identically
when the target is ahead of the counter and additionally handles the
wrap-around case, including a target of zero. The counter update, the
crossing check and the cycle profiling counters are moved together into
a new stepCount() helper so that a single implementation stays
authoritative.
2026-08-26 23:38:43 +01:00
Christopher Bonhage 4bfccfc3d5 nall/gdb: support QPassSignals
The client can now ask the stub to deliver chosen signals without stopping
or reporting (gdb sends QPassSignals automatically for signals configured
'handle <sig> nostop noprint pass'). Advertised in qSupported; the pass set
is client state, cleared on connect/disconnect like breakpoints.
2026-08-25 17:56:11 +01:00
invertego e3b53a6ec9 Merge commit '8a22cd8096c8ce366cccd44813b6210c2160ff29' 2026-08-24 02:23:23 -07:00
invertego 8a22cd8096 Squashed 'thirdparty/sljit/' changes from 666125ad6..de0259c7a
de0259c7a aarch64: set bit 14 for encoding [S|U]MULH (#370)
fdb8e8ce2 Fix build warnings by avoid function pointer to object pointer casts (#368)
67893de6c alpha: Add 64-bit JIT backend (#364)
f8d585b76 ppc: add SLJIT_BREAKPOINT (#367)
8c4127005 ubsan: avoid UB with << of a signed integer (#365)
3907e6900 Add zext.w support for riscv (#363)
d8c86ea66 regexJIT: guard dfa_transitions allocation against size overflow (#361)
d74aa8871 ppc64: Cast mask to sljit_sw in TEST_UI_IMM macro (#360)
d9902b1ba config: prevent aarch64_be from being autodetected (#345)
35ffee1a8 remove smartquotes in C comment (#353)
d034fb107 x86_64: fix near jump check (#352)
acf9f8f85 Move modular header includes outside `extern "C"` scope (#351)
68f01a538 Improve overflow check on RISCV (#350)
8d40e0306 riscv: make sure platform identification for RISCV is succint (#348)
8d0d05c58 Improve carry flag handling (#347)
a8d667f81 Improve signed/unsigned comparison of integers larger than a single register (#346)
c2598a9c2 netbsd: allow mprotect exception for PAX in wxalloc (#344)
3150656bb x86: avoid SIGILL in test25 for older CPUs (#343)
45f910b78 Improve sljit_emit_op2_shift on x86 (#342)
72f95ea56 Small emit_select improvements (#341)
31d2e5308 loongarch: Implement compare select (min/max) operation (#340)
15c64bb72 Implement compare select (min/max) operation on s390x (#339)
39754d085 Implement compare select (min/max) operation (#338)
b7442be1c loongarch: Implement sljit_emit_op2_shift (#337)
f621129c0 \Implement sljit_emit_op2_shift on s390x (#336)
6612fe51e Attempt some further improvements to -Wimplicit-fallthrough support (#331)
f9fd801f4 Implement sljit_emit_op2_shift (#335)
88d0ce0c0 Ensure that some log messages are written to the verbose stream (#333)
c57378efb Correct doc string for sljit_emit_jump (#332)
1bec7a611 config: add SLJIT_FALLTHROUGH to exempt -Wimplicit-fallthrough or C5262 (#330)
e281e199c Add more SLJIT_TMP_OPT_REG tests (#328)
a9e5ec6ae Introducing SLJIT_TMP_OPT_REG (#327)
d1fd994c1 Improve tmp register support (#326)
1fade852b aarch64: avoid -Wunused_parameter in older CPUs (#324)
1232958de Improve the description of sljit_emit_[f]select instructions
7779da89d Add test for dynamic code generation in buffers (#318)
8766c3abf Fix spelling in C code comments (#317)
2e7b6f2a7 Fix Docusaurus URL configuration (#316)
5e42fc93b Rework website (#310)
8b3029792 loongarch: improve immediate generator (#315)
1e66c9054 loongarch: Update toolchain and minor code style change (#314)
f9beb306b Fix sljit_emit_icall on Linux x86_64 (#313)
e93aa7c62 Fix test_buffer1 (#312)
98802d939 Improve jump/op_addr code generation on s390x (#309)
f0a7de957 Implement add address operation (#308)
4ec49e599 Improve CI (#307)
2dd451147 Fix test fails on loongarch (#306)
4c9e95004 Fix test fails on s390x (#305)
5aa4885ee Add more buffer tests (#304)
d7bf1f5b2 Implement read-only buffer allocation (#303)
994c013d6 Implement aligned labels on s390x
06e5b0ed5 Implement aligned labels (#302)
abbb179b1 Consolidate docs (#301)
a442875df Improve rewritable consts on s390x (#300)
f57890048 Improve rewritable consts (#299)
959c13542 loongarch: Fix sljit_emit_single and sljit_emit_op2cmpz (#298)
370630ec0 Implement emit_op2cmp operation (#296)
8bacc9aee Rename doc to docs (#297)
a96da7a86 Improve immediate generation on RISCV (#295)
2984ac7b1 Implement 16 bit branches for RISCV (#294)
d3cec21d9 Implement compressed arithmetic instructions for RISCV (#293)
85782cfb7 Implement load/store compressed instructions for RISCV (#291)
500037920 Implement several compressed instructions for RISCV (#290)
a969602da Better detection of available features on RISCV (#289)
2cdf4edee Change instruction size unit to 16 bit on RISCV (#288)
8481dde36 Enable -Wshadow warning, and rename shadowed variables (#286)
415ddeaea Fix sljit_emit_simd_sign RISCV (#284)
eb8ef1e81 Fix sljit_get_local_base (#282)
e51eabbfb Implement proper argument checks for float/simd registers (#280)
f7037aa15 Improve vector reg support on s390x (#279)
dcd3b10cd Improve vector reg support (#278)
a5ecc9c2e Comment else-s and endif-s in macros (#277)
2cf8a2495 Checks for separated vector registers (#275)
9dc3a4cf2 loongarch: Add SLJIT_UPPER_BITS_SIGN_EXTENDED (#276)
c0fd3d1ae Rename zeroed to zero extended
4b419b4bf Provide information about 32 bit operations on s390x
504672a9f Provide information about 32 bit operations register usage
20edfb055 CI: Update uraimo/run-on-arch-action to 2.8.1 (#274)
446e5c71f loongarch: Optimize jump maximum offset prediction & update action compiler (#273)

git-subtree-dir: thirdparty/sljit
git-subtree-split: de0259c7aaf36aa40cba8014f3fad3edde9307f9
2026-08-24 02:23:13 -07:00
invertego 3918992a41 n64: fix builds with ARES_UNITY_CORES=OFF 2026-08-24 09:37:50 +01:00
invertego f42f60d744 recompiler: fix floating-point issues impacting n64 core (#2611) 2026-08-24 10:00:48 +02:00
Giovanni Bajo fc83454343 n64: rename qnan/snan naming
Ares previous naming was following IEE754 but VR4300 uses an inverted
encoding for NaNs. This also shows that the CPU behavior is more logical.
2026-08-22 09:17:02 +01:00
ZengGengSen f23eb39ad5 a52: add v148 save-state support 2026-08-20 16:37:55 +01:00
ZengGengSen f67757e35e a52: add CX53 Trak-Ball controller 2026-08-20 16:37:55 +01:00
ZengGengSen 190afd8ad2 a52: implement POKEY and standard controller 2026-08-20 16:37:55 +01:00
ZengGengSen e2cdee5038 a52: implement GTIA hardware model 2026-08-20 16:37:55 +01:00
ZengGengSen 85adb9862f a52: implement ANTIC hardware model 2026-08-20 16:37:55 +01:00
ZengGengSen 0a78d55452 a52: implement cartridge media model 2026-08-20 16:37:55 +01:00
ZengGengSen e5d619c238 a52: add Atari 5200 scaffold and CPU 2026-08-20 16:37:55 +01:00
user a840e6427e gba: halt OBJ engine early if DISPCNT bit 5 is set 2026-08-20 16:37:08 +01:00
user 9cb73a6842 gba: implement OBJ VRAM access timings 2026-08-20 16:37:08 +01:00
ZengGengSen 901f57e125 desktop-ui: add configurable digital-to-analog mapping (#2602) 2026-08-20 16:36:55 +01:00
Giovanni Bajo 8da242f5a6 n64: remove hardcoded 8 MiB memory limit in recompiler
Fallback to interpreter or slow-path when accessing out-of-bound memory
2026-08-20 16:36:40 +01:00
Max Bebök fa2105ea11 fix: EEPROM null pointer access 2026-08-20 16:36:19 +01:00
ZengGengSen 30131cd32c Fc/vs unisystem support (#2594)
## Overview

Adds Nintendo Vs. UniSystem support to the ares FC core. Games remain in
MAME
ZIP format and load through the existing Arcade path, without going
through
the iNES detection path used by regular Famicom software.

The implementation covers MAME metadata generation, parent ZIP lookup,
Vs. PPU
and palette behavior, cartridge boards, protection logic, cabinet I/O,
DIP
switches, controller wiring, Zapper input, and save states. A total of
41 Vs.
UniSystem sets have been validated. Vs. DualSystem, non-standard bootleg
hardware, and other configurations that have not yet been implemented
are
explicitly marked as unsupported instead of being started with the
UniSystem
model.

## Why `vsnes.py` is separate

MAME groups three substantially different hardware families in
`nintendo/vsnes.cpp`:

- Vs. UniSystem with one CPU and one PPU;
- Vs. DualSystem with two CPUs and two PPUs;
- non-standard bootleg hardware built around a Z80 and SN76489.

The generic MAME parser reads common data such as machine entries, ROM
regions,
and DIP switches. Those fields alone are not enough to determine which
Vs.
hardware model a game requires. The three families have different
runtime
structures and cannot share a single emulation path.

Configurations also differ between Vs. UniSystem games. The PPU,
palette,
cartridge board, protection logic, controller wiring, Zapper support,
and DIP
settings may all vary by set. The core needs explicit configuration data
rather than inferring hardware from a game title.

`vsnes.py` reuses the MAME XML and ROM-region information produced by
the
generic parser, then adds hardware classification and per-game
configuration.
It generates a deterministic `VsSystem.bml` database. At runtime,
supported
sets select the corresponding FC hardware model from this metadata,
while
DualSystem and bootleg sets are rejected with a clear explanation.

## Video

The combined video shows the attract modes of all 41 supported games.
Each
game is shown for approximately ten seconds, with its name in the
upper-right corner.



https://github.com/user-attachments/assets/46a59b2b-f64a-40ce-a870-1351d5df3948



https://github.com/user-attachments/assets/7927d1d0-4349-4f4b-9092-feeb7b0ac127



https://github.com/user-attachments/assets/d162c834-b227-497b-afe7-2f766f3ab42a



https://github.com/user-attachments/assets/12d7b58e-7cd5-4bbf-b040-9d615e654e0c



https://github.com/user-attachments/assets/a8708059-4369-48e1-be1f-42d15099d7a8


## Screenshots

The contact sheet contains one attract-mode screenshot for each game,
composed
at the original capture resolution.

<img width="3920" height="5328" alt="contact-sheet"
src="https://github.com/user-attachments/assets/e111cf05-4a49-4b0d-9719-082b2b925342"
/>

## Supported sets

The following 41 Vs. UniSystem MAME sets have been validated for loading
and
attract-mode operation:

| No. | MAME short name | Game |
| ---: | --- | --- |
| 1 | `btlecity` | Vs. Battle City |
| 2 | `cluclu` | Vs. Clu Clu Land |
| 3 | `cstlevna` | Vs. Castlevania |
| 4 | `drmario` | Vs. Dr. Mario |
| 5 | `duckhunt` | Vs. Duck Hunt (set DH3 E) |
| 6 | `excitebk` | Vs. Excitebike (set EB4-3 E) |
| 7 | `excitebkj` | Vs. Excitebike (set EB4-4 A) |
| 8 | `excitebko` | Vs. Excitebike (set EB4-3 ?) |
| 9 | `goonies` | Vs. The Goonies (set E) |
| 10 | `hogalley` | Vs. Hogan's Alley (set HA4-1 E-1) |
| 11 | `iceclimb` | Vs. Ice Climber (set IC4-4 B-1) |
| 12 | `iceclimba` | Vs. Ice Climber (set IC4-4 ?) |
| 13 | `jajamaru` | Vs. Ninja Jajamaru Kun (Japan) |
| 14 | `ladygolf` | Vs. Stroke & Match Golf (Ladies Version, set LG4 ?)
|
| 15 | `ladygolfe` | Vs. Stroke & Match Golf (Ladies Version, set LG4 E)
|
| 16 | `nvs_machrider` | Vs. Mach Rider (Endurance Course Version) |
| 17 | `nvs_machridera` | Vs. Mach Rider (Fighting Course Version, set
MR4-1 A) |
| 18 | `nvs_mightybj` | Vs. Mighty Bomb Jack (Japan) |
| 19 | `nvs_platoon` | Vs. Platoon |
| 20 | `rbibb` | Vs. Atari R.B.I. Baseball (set 1) |
| 21 | `rbibba` | Vs. Atari R.B.I. Baseball (set 2) |
| 22 | `skatekds` | Vs. Skate Kids. (Graphic hack of Super Mario Bros.)
|
| 23 | `smgolf` | Vs. Stroke & Match Golf (Men Version, set GF4-2 F) |
| 24 | `smgolfb` | Vs. Stroke & Match Golf (Men Version, set GF4-2 ?) |
| 25 | `smgolfj` | Vs. Stroke & Match Golf (Men Version) (Japan, set GF3
B) |
| 26 | `starlstr` | Vs. Star Luster |
| 27 | `suprmrio` | Vs. Super Mario Bros. (set SM4-4 E) |
| 28 | `suprmrioa` | Vs. Super Mario Bros. (set ?, harder) |
| 29 | `supxevs` | Vs. Super Xevious |
| 30 | `tkoboxng` | Vs. T.K.O. Boxing |
| 31 | `topgun` | Vs. Top Gun |
| 32 | `vsfdf` | Vs. Freedom Force |
| 33 | `vsgradus` | Vs. Gradius (US, set GR E) |
| 34 | `vsgshoe` | Vs. Gumshoe (set GM5) |
| 35 | `vspinbal` | Vs. Pinball (US, set PN4 E-1) |
| 36 | `vspinbalj` | Vs. Pinball (Japan, set PN3 B) |
| 37 | `vsskykid` | Vs. Super SkyKid |
| 38 | `vsslalom` | Vs. Slalom |
| 39 | `vssoccer` | Vs. Soccer (set SC4-2 A) |
| 40 | `vssoccera` | Vs. Soccer (set SC4-3 ?) |
| 41 | `vstetris` | Vs. Tetris |
2026-08-20 16:35:50 +01:00
ZengGengSen b80f67d383 input: preserve relative mouse motion between polls (#2593)
## Changes

- Accumulate relative mouse movement until it is read and consumed by an
input mapping.
- Maintain an independent read position for each `InputRelative`
binding.
- Synchronize relative input positions when input bindings or mouse
capture state changes.
- Apply the same relative-motion handling to the Raw Input, NSEvent, and
Xlib mouse drivers.

## Motivation

In my testing, `InputManager::poll()` is called approximately once every
8ms on my system, with observed intervals generally no shorter than 5ms.
At 60Hz, each frame lasts approximately 16.67ms, so input is normally
polled twice per frame.

Each poll retrieves the mouse-axis movement accumulated during the
preceding approximately 8ms. However, emulator cores generally read
relative input only once per frame. This means that `InputManager` may
complete its second poll before the core reads the movement value.

The previous implementation retained only the mouse-axis movement
reported by the most recent poll. The second poll therefore overwrote
the result of the first, allowing the core to observe only the movement
from the latest approximately 8ms. Mouse movement from the earlier
approximately 8ms of the frame was lost.

This change accumulates all relative mouse movement produced between
consecutive core input reads. As a result, even when
`InputManager::poll()` runs multiple times within a single frame, the
core can still retrieve the complete mouse-axis movement accumulated
during that period.
2026-07-30 20:29:09 +01:00
ZengGengSen 53ba720c8d ruby: normalize Windows Raw Input mouse motion (#2592)
## Abstract

Support the absolute mouse movement events in the native input driver
for Windows.
Modifications:

- Convert the absolute original input coordinates to relative motion
increments;
- Handle the coordinate spaces of the main desktop and virtual desktop
while maintaining independent absolute coordinate positions;
- Add locking in the `poll` function to prevent partial data loss in the
event of multi-threaded competition.

## Reasons 

When I used 'Windows Remote Desktop Connection' for remote debugging, I
found that 'win raw input' did not provide the corresponding 'relative
mouse event'. Since 'ruby' ignored the 'absolute mouse event', the input
relying on the mouse (such as the crosshair of a gun sight) could
receive key operations, but it was unable to move.
2026-07-29 10:25:01 +01:00
Roger SandersandRoger Sanders 274aabdf4a megald: Fix NEC LaserActive fader settings in VDP burst mode (#2591)
Fixes a video mixing issue which affected graphics in the "Demon's
Judgement" LDRom2 game for the Pioneer LaserActive with the NEC PAC.

---------

Co-authored-by: Roger Sanders <[email protected]>
2026-07-29 09:39:10 +01:00
Luke Usher cdabe5c5a6 build: grant release action wire permissions to release artifacts 2026-07-25 19:12:02 +01:00
Giovanni Bajo 5fdc7a8c0b n64: fix in-block self-modifying code with JIT 2026-07-24 16:47:33 +01:00
Giovanni Bajo ec5e82ae76 n64: fix RDP buffer busy after flush 2026-07-24 16:47:33 +01:00
Giovanni Bajo 1bad216467 n64: implement various not implemented exceptions 2026-07-24 16:47:33 +01:00
Giovanni Bajo 1fcef61b84 n64: fix COP0 TagLo latching unused bits, and MTC0 writing 64-bit values 2026-07-24 16:47:33 +01:00
Giovanni Bajo f1382f7780 n64: other accuracy improvements to CACHE opcode (icache) 2026-07-24 16:47:33 +01:00
Giovanni Bajo 6ed19d3115 n64: improve accuracy of various corner cases of the CACHE opcode
Specifically, the dirty flag behaves differently from the "obvious" way,
and cannot be affected by the CACHE opcode.
2026-07-24 16:47:33 +01:00
Giovanni Bajo 7806042b31 n64: SC/SCD do not clear llbit 2026-07-24 16:47:33 +01:00
Giovanni Bajo 433d07fbdb n64: implement banked SRAM support (fixes Dezaemon 3D saves) 2026-07-24 07:50:27 +01:00
Giovanni Bajo 5c6607fa9b n64: rewrite flash emulation
This uses all the latest research on the exact behavior of flash chips,
including quirks of specific models, as documented on n64brew.

Thanks to the new PI bus emulation, it is now possible to correctly
implement many quirks "naturally", because of the simple way flash
chips interact with the bus.
2026-07-24 07:50:27 +01:00
Giovanni Bajo 3c63fbcfb4 n64: rewrite PI bus implementation to make it closer to the hardware
PIDevice is now a generic device sitting on the PI bus that is able
to reply to addresses in any range. Now each device knows the addresses
it is listening to, which is how the hardware works.

PI accesses are only 16-bit wide, and both DMA and CPU I/O are built
upon it. There is no more code to mimic the weird access patterns for
misaligned addresses or different access sizes, as everything descends
naturally from the way the bus works (as it should be).

Open bus is simulated by latching the last value on the bus, which is
what happens on hardware.

This opens the door to more accurate emulation of chips which abuse the
bus in weird ways (like flash memories).
2026-07-24 07:50:27 +01:00
Giovanni Bajo 45c2291261 n64: implement access to RDRAM hidden bits
We always update hidden bits on memory writes. Given that in most cases
this is done during dcache flushes, we use SIMD to optimize that code
path and update a whole cacheline's worth with a few opcodes.

Also take the chance to add a mi/bus.hpp that implements the special
MI modes (ebus, repeat, upper), so that they naturally only apply
to CPU-issued reads.
2026-07-24 07:50:16 +01:00
Giovanni Bajo 3cac8a1863 n64: emulate RDRAM chip registers and behaviors
This finally allows Ares to fully run through the IPL3 to fully initialize
the RAM chips. It tries to simulate also all the various non-working
conditions so that a change in a IPL3 that results in failure to boot,
should also fail in Ares. Obviously this would be a very long journey,
but at least all major failures are covered here.

Default RDRAM linear mapping is still kept as default as a fast path
in the recompiler to avoid slowdowns in normal situations. Given
that parallel-RDP doesn't support non-linear mappings, we just emit
a warning in that case.

Invalid current values will result in degraded RAM accesses like on real
hardware. This is also necessary to fully pass the current calibration
algorithm and to provide entropy (collected by libdragon's IPL3).
2026-07-24 07:50:16 +01:00
Giovanni Bajo d5c0568492 ares: add "Deterministic Entropy" option and hook it up to sfc and n64 2026-07-24 07:50:16 +01:00
Roger Sanders 0b9b8bce6b megald: Fix LaserActive support after recent cd changes, plus additional fixes. 2026-07-24 07:50:04 +01:00
T3hD0gg 0583153727 gdb: Fix single step & resume packet address parsing to match GDB specification (hex instead of integer). 2026-07-24 07:47:54 +01:00
T3hD0gg 1cc7dfd4f6 gdb: Fix read register packet index parsing to match GDB specification (hex instead of integer). 2026-07-24 07:47:54 +01:00
Bryce Wilson e1b0d47440 n64: enable gdb reads of sizes that are not multiples of 4 (#2519)
If we hit the final branch of the if in the read hook and `byteCount` is
not a multiple of 4, the remaining bytes are never read, but the buffer
is still sent back with unset data. This adds one more iteration to the
loop if necessary to read another word, and then shrinks the string to
the correct size.

In the write hook, the simple cases will do a write and then break the
switch and end up in the conditionals, potentially writing twice. So I
switched the breaks to returns.

The final conditional code in the write hook also cannot handle data
sizes that aren't multiples of 4 bytes, but I'm less confident about how
to fix that. Happy to add it to this PR if someone who knows what
they're doing advises.

Also trimmed some trailing whitespace.
2026-07-23 02:28:11 +02:00
user be3c7898ce arm7tdmi: prevent STM writeback from modifying r15 2026-07-19 09:19:38 +01:00
thekovic 449b93716f n64: Remove field swapping workaround for interlaced image (#2574)
Before we were drawing the frame according to the VI registers at line
0. After a recent change (I believe
https://github.com/ares-emulator/ares/commit/31526ded9196b046b0b5ce1769f14335eba9cc33
), we now draw it according to VI registers when display area begins.
This means that we take into account the vblank interrupt that normally
changes the registers so the field swapping workaround is not needed
anymore.
2026-06-28 00:59:25 +02:00
Stefan Schlosser 6dc3f3306c ruby: improve SDL3 audio performance
Some platforms experienced very poor performance with the SDL3 audio
driver, especially in combination with a demanding core like the N64.
It is assumed that SDL couldn't keep up filling the output device buffer
with the audio data from the input queue buffer. Increasing the amount
of generated input data before feeding it to the device buffer has shown
to greatly improve the performance.

This sparked the following idea and change to how the SDL3 audio driver
fills its buffers: request a fixed-sized 10ms device buffer and apply
the latency to the input buffer, i.e. the input buffer holds a multiple
of 10ms worth of audio. This allows SDL to steadily consume audio from
the input buffer and feed it into the 10ms device buffer, while the user
can still control the latency.

Additionally add the value 30 to the possible latency options. This
allows users to specify a lower latency than 40 if 20 is just a tiny bit
shy of being good enough.
2026-06-24 15:43:37 +01:00
remutro 91b112279a Fix calculation 2026-06-14 20:31:35 +01:00
remutro c634ac3639 Update available rewind options 2026-06-14 20:31:35 +01:00
remutro 743ef1f2cd Expose rewind controls in the UI 2026-06-14 20:31:35 +01:00
Stefan Schlosser 2e7a877a96 Fix error: undeclared identifier 'renderSettingsLabel'
The renderSettingsLabel identifier isn't declared anymore after the
recent structural changes, but it's still referenced conditionally in
desktop-ui/settings/cores.cpp and breaks the build if the N64 core is
disabled:

/tmp/ares/desktop-ui/settings/cores.cpp:108:3: error: use of undeclared identifier 'renderSettingsLabel'
  108 |   renderSettingsLabel.setCollapsible(true).setVisible(false);
      |   ^

Remove it to fix the build.
2026-06-14 12:47:22 +01:00
remutro 1337a5c013 Use windows-latest image 2026-06-13 23:24:18 +01:00
remutro 459ceb2e22 Update Visual Studio related cmake generators to VS2026 2026-06-13 23:24:18 +01:00
remutro 0d14e49319 Update Visual Studio related cmake generators to VS2026 2026-06-13 23:24:18 +01:00
Stefan Schlosser 9293695459 CI: Cleanup build dependencies on Linux/FreeBSD
Remove the build dependencies of the removed audio and input drivers,
now that SDL3 is the only supported driver.

Additional cleanup on FreeBSD:

- libX* and libglvnd are dependencies of sdl3 itself and can reasonable
  be expected to continue to be so. This allows to remove them from the
  list of explicit bulid dependencies.

- vulkan-loader provides libvulkan.so which is a dlopen'ed runtime
  dependency of parallel-rdp used in the N64 core. It is not a build
  dependency and can be removed (though it gets installed anyway as a
  dependency of sdl3).
2026-06-13 23:23:08 +01:00
remutro 5f2f7dc0d4 Update Debug to Developer panel, GPU sync enabled with video sync 2026-06-12 19:04:14 +01:00
remutro 7032bc6ef0 Complete SDL device enum and settings reorganization 2026-06-12 19:04:14 +01:00
remutro acf213fa5f Causing crashouts due to initial settings reorg 2026-06-12 19:04:14 +01:00
remutro 4ceadb9549 SDL device enum, starting moving settings 2026-06-12 19:04:14 +01:00
remutro 2ce75dde67 Restore audio device setting 2026-06-12 19:04:14 +01:00
Giovanni Bajo 31526ded91 n64: reduce 1 frame of input latency, and fix flickering when CPU drawing
We were telling Vulkan to display a frame as per the VI configuration
at vblank (line 0). This is wrong since all games will actually use
vblank to configure VI for the new frame. Vulkan would thus draw the
*previous* frame, rather than the new one that was just created, causing
one full frame of latency. The correct point where to kick Vulkan is at
vstart, when the active display begins.

With CPU-based rendering in a double-buffering scenario (rather than
triple buffering), this means that the wrong framebuffer would always
be shown: the one that was just going to be drawn upon. Thus, we would
basically get a race between the emulated CPU writing to RDRAM and the
host Vulkan reading RDRAM.

The bug was hidden when using RDP rendering because RDP emulation also
goes through Vulkan, so it would basically be queued *after* the VI
drawing rather than *during* it.
2026-06-09 22:31:32 +01:00
remutro c3c2b79015 Squash audio abstractions into a single audio struct 2026-06-09 13:32:43 +01:00
remutro 8c41639a76 Increase bytes to wait in SDL audio to help performance 2026-06-09 13:32:43 +01:00
remutro 8bcb10f36b Minor cleanup 2026-06-09 13:32:43 +01:00
remutro d6430da5dc Cleanup dependencies 2026-06-09 13:32:43 +01:00
remutro 57a4bec25d Fix FreeBSD build 2026-06-09 13:32:43 +01:00
remutro 920dfb3de8 fix sdl build script 2026-06-09 13:32:43 +01:00
remutro c02233f815 fix script permission 2026-06-09 13:32:43 +01:00
remutro 0ec4b5f953 Remove plugin related functions, add SDL build 2026-06-09 13:32:43 +01:00
remutro aa5df08e2b Remove driver list controls from settings 2026-06-09 13:32:43 +01:00
remutro e70cb9fb58 SDL fixed 10ms latency 2026-06-09 13:32:43 +01:00
remutro 40474c4987 Fix MacOS build 2026-06-09 13:32:43 +01:00
remutro 53d8b81ade Update non-windows cmake files 2026-06-09 13:32:43 +01:00
remutro bb3c36cd96 Delete unused files 2026-06-09 13:32:43 +01:00
remutro 5ee7e9a5ba Make SDL the only available audio & controller input driver 2026-06-09 13:32:43 +01:00
Giovanni Bajo 6fcaa213fb n64: in emux, add basic profiling for RSP (cyles and halted cycles) 2026-06-09 13:32:24 +01:00
Giovanni Bajo 658aad5dd7 n64: in emux XPROFREAD, fix a bug in accessing the global slot 2026-06-09 13:32:24 +01:00
Giovanni Bajo e67f0cefc5 n64: DIV/DIVU in user mode never raise a RI exception 2026-06-04 00:52:24 +01:00
remutro 90c655b944 Update dependencies (post-v148) 2026-05-31 09:08:00 +01:00
remutro 0aafd85789 Fix build warning 2026-05-28 13:42:30 +01:00
Luke Usher 67b0bf7aac ruby: improve SDL audio sync on non-stereo devices
Use the application stream format rather than the physical device format.

Some outputs, such as HDMI/TV devices with Dolby processing enabled,
may expose a multichannel device format even though ares is submitting
stereo float audio to SDL. Using the device format for queue sizing and
drain timing could make the blocking sync path sleep for the wrong
amount of time, causing the emulator to run too slowly.

General cleanup:
 - avoid heap allocation for every audio packet
 - add proper failure checks
 - correctly teardown SDL_Audio on shutdown
2026-05-28 08:47:04 +01:00
remutro 11d887c99c Fix aspect correction menu radio items in GTK 2026-05-28 08:10:27 +01:00
Luke Usher 7c03fb208f desktop-ui: add tape position slider 2026-05-25 16:23:20 +01:00
Luke Usher b33edf5f90 sg/fc/msx/spec: update serialization for tapes 2026-05-25 16:16:05 +01:00
Luke Usher 83369fd40f sg: add tape support to sc-3000 2026-05-25 16:11:50 +01:00
Luke Usher 6ee42f25b4 msx: hook up tape recording to tape manager 2026-05-25 15:39:46 +01:00
Luke Usher fe388a7fcc desktop-ui: fix tape recording button attribute lookup 2026-05-25 15:33:23 +01:00
Luke Usher c7d667fa07 ruby: fix keyboard mappings on macOS by adding missing left/right modifier keys 2026-05-25 11:36:27 +01:00
Luke Usher cc0e405780 msx: use the 'new' tape system rather than hard-coding the tape deck, add support for tsx/tzx and cas.
also prevent creating a .wav when not writing
2026-05-25 10:07:32 +01:00
Luke Usher c21adc7b57 spec: use the 'new' tape system rather than hard-coding the tape deck 2026-05-25 09:25:10 +01:00
Luke Usher 232f85e39c desktop-ui: move dynamic scale options to a vector rather than a grouo
Fixes being able to select multiple scaling-related options simultaneously
2026-05-24 23:40:30 +01:00
Luke Usher c951652efb desktop-ui: fix broken virtualpad overrides 2026-05-24 23:02:26 +01:00
Luke Usher f605d52032 desktop-ui: replace multiplier sizing with viewport sizing and re-add manual integer scaling ratios
Replace the old fixed multiplier window sizing model with configurable
viewport sizing.

Window sizes are now stored as target viewport dimensions rather than
being derived from the emulated screen. Standard presets have been
provided, as well as the option to specify a custom size.

Adaptive sizing has been reworked to treat the window size as target
bounds, the actual window will dynamically grow or shrink within this
boundary to preserve the correct aspect ratio.

Oversized window recovery has been changed to respect aspect ratio
and available workspace rather than a hardcoded reducton from the
monitors geometry.
2026-05-24 20:29:39 +01:00
Luke Usher bdd585a563 hiro: cleanup some macOS compile warnings and deprecation notices 2026-05-24 17:57:20 +01:00
Luke Usher 54fc194182 desktop-ui: persist SDL joypad mappings by GUID slot
Add an optional stable text identifier to HID devices and use it for
persisted input mappings when available.

SDL joypads now expose a GUID/slot identifier, so newly saved controller
mappings no longer depend on SDL serial or device path values that can
change across disconnects, reconnects, or app launches. This keeps
multiplayer controllers distinguishable by assigning slots per matching
GUID while avoiding unstable path-derived IDs.

The input mapping parser remains backward compatible with existing
numeric 0x<deviceID>/<group>/<input> mappings. Drivers that do not set a
stable identifier continue to save and load mappings using the existing
numeric HID device ID format.
2026-05-24 17:38:35 +01:00
user 4c39ca23eb gba: skip rendering sprites that are offscreen horizontally 2026-05-24 17:02:16 +01:00
user 8ea87c75e4 gba: implement OAM stage timings for OBJ engine 2026-05-24 17:02:16 +01:00
Morilli 7f0f91b3da sfc: fix ppu horizontal offset in high-resolution offset-per-tile mode 2026-05-23 16:00:34 +01:00
Luke Usher 164f5648bf ruby: stabilize joypad identity and duplicate device handling for the SDL driver 2026-05-21 14:45:20 +01:00
Luke Usher 19cce7dbba ld: fix LaserActive video seeks after CD-ROM refactor 2026-05-20 20:05:50 +01:00
Giovanni Bajo 765c0d4181 n64: fix partial load/stores in little-endian mode
Uncovered by new tests in n64-systemtest. The JIT wasn't tested because
little-endian is only supported in user-mode for now (RE=1), where
memory accesses must go through TLBs so they currently are not JIT'd.
Anyway the fix should be correct if we eventually uncover full
little-endian mode (with BE=0).
2026-05-18 19:44:25 +01:00
Luke Usher fbc8f22821 desktop-ui: default overscan to true
More natural window aspect by default, especially in PAL regions.
2026-05-17 19:41:04 +01:00
Luke Usher 14f4fc0173 desktop-ui: add direct input mapping mode with VirtualPad fallback
(AKA core-specific input mappings!)

Also adds support for direct input devices that bypass VirtualPad entirely, currently used for Mahjong
2026-05-17 19:40:33 +01:00
user 6920622b7e gba: add OAM to memory viewer 2026-05-17 19:17:02 +01:00
Giovanni Bajo c42bb2626d n64: implement VR4300 reverse-endian mode
This is a mode available only in the user mode, activated when RE=0. It
facilitates running MIPS binary code that was build for a little-endian
architecture, assuming that the code/data was 8-byte swapped in memory
before running it.

It passes the new RE tests in n64-systemtest
2026-05-17 19:16:48 +01:00
Morilli d45be3297f sfc: Improve autojoypad polling accuracy 2026-05-11 10:57:30 +01:00
remutro 54adf82264 Update libchdr to version 0.3.0 2026-05-11 10:56:14 +01:00
remutro 7d1bbf7c04 Update libchdr to version 0.3.0 2026-05-11 10:56:14 +01:00
remutro d17ae2f36d Update libchdr to version 0.3.0 2026-05-11 10:56:14 +01:00
remutro b9f8347d57 Update libchdr to version 0.3.0 2026-05-11 10:56:14 +01:00
Giovanni Bajo 41af52ec27 n64: improve CPU JIT documentation 2026-05-11 10:53:35 +01:00
Giovanni Bajo 87daf87fbb n64: decrease a bit jit interleaving (unfreeze conker) 2026-05-11 10:53:35 +01:00
Giovanni Bajo 2a5b9fe74e n64: fix interrupt generation to never happen mid-instruction 2026-05-11 10:53:35 +01:00
Giovanni Bajo bb91ff1136 n64: fix a few cases where interrupt latency was still higher than expected 2026-05-11 10:53:35 +01:00
Giovanni Bajo 5d7496c6f1 n64: change CPU JIT flush message 2026-05-11 10:53:35 +01:00
Giovanni Bajo 18692c3ced n64: increase JIT interleaving and add more forced syncs on MMIOs 2026-05-11 10:53:35 +01:00
Giovanni Bajo bc3af76716 n64: reduce interrupt latency to zero in JIT 2026-05-11 10:53:35 +01:00
Giovanni Bajo 03dd55bbda n64: in CPU JIT, correctly declare 6 scratch registers (and don't use 7th) 2026-05-11 10:53:35 +01:00
Giovanni Bajo ffab36ed99 n64: in CPU JIT, verify internal assumptions on block links 2026-05-11 10:53:35 +01:00
Giovanni Bajo 8ad8803765 n64: in CPU JIT, implement LL/SC/LLD/SCD 2026-05-11 10:53:35 +01:00
Giovanni Bajo 3198e0e88e n64: in CPU JIT, rewrite branches to use branchless code 2026-05-11 10:53:35 +01:00
Giovanni Bajo 999d858fa7 n64: in CPU JIT, extend blocks also beyond jump and links 2026-05-11 10:53:35 +01:00
Giovanni Bajo a752515d2d n64: bump serializer 2026-05-11 10:53:35 +01:00
Giovanni Bajo 2460b01b27 n64: in CPU jit, synchronize CPU/RSP on status and semaphore I/O 2026-05-11 10:53:35 +01:00
Giovanni Bajo 2af310528b n64: in CPU JIT, fix LWR.
Also fix it in interpreter to align to newly tested corner case
(non sign-extended input).
2026-05-11 10:53:35 +01:00
Giovanni Bajo a1be16873f n64: in CPU JIT, compute homebrew dcache dirty mask after stores
Avoid keeping the dirty-byte mask live across store fast paths in
jitMemoryOpcode. Compute it at the end from Rs+i16 so temporary
register clobbers (eg. SDC1/SWL/SWR) cannot corrupt dirty tracking and
lead to stale cache data.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 8b4f1fa16e n64: CPU now has configurable interleaving with the rest of the hardware 2026-05-11 10:53:35 +01:00
Giovanni Bajo 882fc4df1f n64: split recompiler in three files 2026-05-11 10:53:35 +01:00
Giovanni Bajo 997077527b n64: in CPU JIT, cleanup to remove redundant structure fields 2026-05-11 10:53:35 +01:00
Giovanni Bajo dd834d2c5d n64: revisit integration between JIT and gdb to improve performance 2026-05-11 10:53:35 +01:00
Giovanni Bajo 50b9786587 n64: make sure events inserted in the queue affect JIT timings 2026-05-11 10:53:35 +01:00
Giovanni Bajo fbb6e72e02 n64: in CPU JIT, fix a bug in TLB handling (eg: Linux)
Now that the JIT can hardcode PC in generated code, each block is valid
for a single vaddr. It can't be reused.
2026-05-11 10:53:35 +01:00
Giovanni Bajo c152ef5639 n64: in CPU JIT, simplify decoder removing dead code 2026-05-11 10:53:35 +01:00
Giovanni Bajo 49af8ab6a2 n64: in CPU JIT, blocks now extend through branches
Terminals are now only hard jumps and the special opcodes that change
state key, timers, etc.

Internal branch targets in each block are also registered as JIT
entrypoints.

Opportunistic branch linking is now removed as not useful anymore.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 68134837d4 n64: avoid excessive JIT trashing by reducing the 4K section granularity 2026-05-11 10:53:35 +01:00
Giovanni Bajo bb75c2f058 n64: in CPU JIT, do not emit pipeline prologue for linear instructions 2026-05-11 10:53:35 +01:00
Giovanni Bajo 105e8da924 n64: in CPU JIT, make epilogue code conditional in fast paths 2026-05-11 10:53:35 +01:00
Giovanni Bajo b0de6c11d2 n64: in CPU JIT, return pipeline information from emit functions 2026-05-11 10:53:35 +01:00
Giovanni Bajo b89a0bcce1 n64: in CPU JIT, add SDL/SDR/SWL/SWL/SWC1/SDC1 2026-05-11 10:53:35 +01:00
Giovanni Bajo 04f8b9cb4d n64: implement LWL, LWR, LDL, LDR, LWC1, LDC1 in JIT 2026-05-11 10:53:35 +01:00
Giovanni Bajo c139a4d4ad n64: in CPU JIT, use JIT-time PC for branches as much as possible 2026-05-11 10:53:35 +01:00
Giovanni Bajo 28f05b0297 n64: in FPU JIT, optimize comparisons 2026-05-11 10:53:35 +01:00
Giovanni Bajo 39b5d798b9 n64: in FPU JIT, implement compare opcodes (first draft) 2026-05-11 10:53:35 +01:00
Giovanni Bajo 7e5841d84a n64: in FPU JIT, add MOV.S/MOV.D implementation 2026-05-11 10:53:35 +01:00
Giovanni Bajo 8c3961cd6e n64: in FPU JIT, add comments and slight tweaks to underflow fixup 2026-05-11 10:53:35 +01:00
Giovanni Bajo e6517f5f57 n64: in FPU JIT, reduce input checks required for conversion opcodes 2026-05-11 10:53:35 +01:00
Giovanni Bajo cd604bf619 n64: in FPU JIT, reduce fallbacks to C++ for conversions in most occurrences 2026-05-11 10:53:35 +01:00
Giovanni Bajo 85008a663e n64: in FPU JIT, simplify a bit conversion opcodes 2026-05-11 10:53:35 +01:00
Giovanni Bajo 8031dcd3f2 n64: fix a benign typo in FCVT_D_L
It went unnoticed because it's basically re-running the same conversion
of the CHECK_FPE line, and probably rounding modes don't matter enough.
2026-05-11 10:53:35 +01:00
Giovanni Bajo f0b8a36b6a n64: implement float/int conversions in FPU JIT 2026-05-11 10:53:35 +01:00
Giovanni Bajo 36aed53124 n64: in FPU JIT, implement ALU double opcodes 2026-05-11 10:53:35 +01:00
Giovanni Bajo 1d8c89a50f n64: refactoring and cleanup of FPU JIT opcode emission 2026-05-11 10:53:35 +01:00
Giovanni Bajo 60007e8853 n64: in FPU JIT, implement FADD.S and FSUB.S 2026-05-11 10:53:35 +01:00
Giovanni Bajo 3542371b76 n64: in FPU JIT, unity 1/2 arity codepaths 2026-05-11 10:53:35 +01:00
Giovanni Bajo a8882a1d89 n64: in FPU JIT, add support for MUL.S and DIV.S 2026-05-11 10:53:35 +01:00
Giovanni Bajo 86fa83fc2b n64: in FPU JIT, implement passthrough of flag bits from host processor 2026-05-11 10:53:35 +01:00
Giovanni Bajo 71ddda52b5 n64: simplify FPU JIT by moving some code into nall/recompiler 2026-05-11 10:53:35 +01:00
Giovanni Bajo be0504e1c2 n64: start implementation of FPU JIT for amd64 too 2026-05-11 10:53:35 +01:00
Giovanni Bajo a34a6ab1df n64: implement initial FPU JIT, for ARM64 2026-05-11 10:53:35 +01:00
Giovanni Bajo 515cb06729 n64: in CPU JIT, remove the deferred pc/nextpc/pipeline materialization
While provably useful, the implementation is completely confusing. We
need to rework it from scratch at some point. The way it is written
now, it impedes further progress in other areas.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 2d852d6a9b n64: refactor to remove obsolete decoder JIT flags 2026-05-11 10:53:35 +01:00
Giovanni Bajo 5b8abb5717 n64: in JIT, add optimizations for accesses via $sp/$gp 2026-05-11 10:53:35 +01:00
Giovanni Bajo 496409c199 n64: implement also memory store opcodes in JIT 2026-05-11 10:53:35 +01:00
Giovanni Bajo 9e6be60b9e n64: implement all aligned load opcodes in JIT 2026-05-11 10:53:35 +01:00
Giovanni Bajo fdbd036bd1 n64: in JIT, remove confusing callf macro 2026-05-11 10:53:35 +01:00
Giovanni Bajo f241c4095d n64: optimize dcache to use a single word with both tag and valid bit 2026-05-11 10:53:35 +01:00
Giovanni Bajo 094454e52f n64: add JIT for LW as a first experiment 2026-05-11 10:53:35 +01:00
Giovanni Bajo dabeb68387 n64 cpu: inline MFC1/MTC1/DMFC1/DMTC1 in the JIT when COP1 is enabled.
Emit loads and stores directly against fpu state via sreg(2), using the same
full- versus half-mode register layout as the interpreter (word selection and
pairing). COProcessor-unusable blocks still call the C++ helpers. Skip GPR
writes when MFC1/DMFC1 targets $zero. FPU word offsets use CPU::r64 in offsetof
to avoid colliding with the host Real type at file scope.
2026-05-11 10:53:35 +01:00
Giovanni Bajo cc82697f00 n64: in CPU JIT, inline icache management 2026-05-11 10:53:35 +01:00
Giovanni Bajo e3f4d6dc0d nall: add mov128 and mem0 for absolute addressing 2026-05-11 10:53:35 +01:00
Giovanni Bajo b698206d07 n64: in CPU JIT, inline fastpath of icache hit check 2026-05-11 10:53:35 +01:00
Giovanni Bajo 7451ccbc98 n64: in CPU JIT, simplify branch link management 2026-05-11 10:53:35 +01:00
Giovanni Bajo 6448413adf n64: JIT multiply/divide SPECIAL opcodes via SLJIT
Add generic LMUL and DIVMOD helpers (32/64, signed/unsigned) and emit fast paths for MULT/MULTU, DIV/DIVU, DMULT/DMULTU, DDIV/DDIVU. Reserved-instruction and divide-by-zero cases stay on interpreter slow paths; DDIV avoids host INT64_MIN/-1 idiv overflow. Decoder no longer marks these opcodes as JitUseCallf or JitAddsExtraCyclesInternally.
2026-05-11 10:53:35 +01:00
Giovanni Bajo d91f838e3b n64: lazily emit callf setup in CPU JIT
Move callf synchronization to the paths that actually emit helper calls, and keep slow-path callf state restoration explicit. Rename jitMayCallf to jitUseCallf so the flag name matches its eager-call semantics.
2026-05-11 10:53:35 +01:00
Giovanni Bajo fc820aba6b n64: centralize JIT callf setup
Route opcode helper calls through a shared setup path that materializes PC and flushes deferred state on demand, so callf overhead is emitted where calls actually occur.
2026-05-11 10:53:35 +01:00
Giovanni Bajo a61dba8ec8 n64: JIT 64-bit shifts and trap opcodes
Move trap exception cases to deferred slow paths and generate direct JIT code for the remaining 64-bit shift immediates, keeping reserved-instruction handling and R0 writeback behavior on the fast path.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 025cde8961 n64: JIT more 64-bit integer ALU operations
Expand CPU JIT codegen for DADDIU, DSLLV/DSRLV/DSRAV, DADDU, DSUB, and DSUBU while preserving exact overflow and reserved-instruction behavior through helper fallbacks. Factor 64-bit reserved-instruction gating into a shared helper and collapse duplicated slow-path conditions to keep hot paths compact.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 2f8659a375 n64: optimize CPU JIT ALU slow paths
Move ADD/ADDI/DADD/DADDI hot paths to direct codegen with deferred slow-path helpers so overflow and reserved-instruction behavior stay exact without polluting the hot block body. Centralize state-key bit semantics in a typed accessor wrapper so JIT-time folding no longer depends on hardcoded bit masks.
2026-05-11 10:53:35 +01:00
Giovanni Bajo f1eff7d2f0 n64: gate JIT pipeline state machinery on demand
Emit state test/update and endblock jumps only when branch/helper pipeline semantics are active, removing these control-flow overhead ops from the linear hot path.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 0d3caa546c n64: defer nextpc materialization in JIT hot path
Accumulate linear nextpc advances and flush only when current PC is needed, removing per-opcode nextpc materialization on the fast path. Also rematerialize helper call PC directly from vaddr in the linear non-delay-slot case.
2026-05-11 10:53:35 +01:00
Giovanni Bajo e7444fd852 n64: lazily commit ipu.pc in JIT loop
Commit architectural PC only for branch/helper/terminal paths while preserving exception PC correctness for helper calls and delay slots. This removes unconditional per-opcode ipu.pc stores on the linear hot path.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 55ac593102 n64: materialize CPU pipeline pc only when needed
Keep pipeline pc writes on branch/helper paths, and commit linear opcode ipu.pc from nextpc-4 to reduce per-opcode JIT overhead while preserving n64-systemtest behavior.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 4ed8a7792c n64: flush deferred CPU cycles only on branch/helper opcodes
Skip per-opcode deferred-cycle flushes for straight-line instructions while keeping EndBlock checks unchanged, reducing hot-loop helper-call overhead without regressing system tests.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 161f01fa0d n64: restore unconditional CPU JIT endblock checks
Revert conditional EndBlock test and epilogue gating in the CPU JIT loop to preserve control-flow behavior and keep the system tests passing.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 606151f79d n64: add virtual PC materialization in CPU JIT loop 2026-05-11 10:53:35 +01:00
Giovanni Bajo b00b1778d1 n64: avoid unconditional JIT step flush in hot loop
Only flush deferred cycles at the per-opcode boundary when an EndBlock check is needed, reducing helper-call overhead on straight-line JIT code.
2026-05-11 10:53:35 +01:00
Giovanni Bajo dc4af1ddad n64: gate JIT EndBlock checks on branch and helper ops
Skip emitting EndBlock test/jump in straight-line instructions where state cannot request block termination, reducing hot-loop overhead while preserving control-flow correctness.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 6b88fdfbbf n64: remove CPU JIT metrics instrumentation
Drop JIT metrics counters and reporting from the CPU recompiler to keep the hot-path code easier to read while preserving runtime behavior. This also keeps the architectural state commit path in emit() minimal by removing duplicated commit emission.
2026-05-11 10:53:35 +01:00
Giovanni Bajo edf712f2f6 n64: extend JIT branch linking to dual targets
Model taken and not-taken link edges per block so conditional branches can be linked by final PC, and add finer diagnostics for no-direct-link causes. Also inline BC1 JIT branch handling with deterministic cause clearing layout checks to reduce unsupported FPU branch cases without regressing exception behavior.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 70f1cf07c5 n64: add safe intra-section JIT block linking
Link JIT blocks lazily within sections with state-key matching and pending backpatches to cut dispatcher overhead. Gate linked jumps on dirty sections, clock/queue budget checks, and unsafe delay slots while adding detailed diagnostics on link misses and candidate coverage.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 6d6c5d5fad n64: refactor CPU JIT to introduce 4K sections and state key 2026-05-11 10:53:35 +01:00
Giovanni Bajo 3a44a460bd n64: add JIT documentation 2026-05-11 10:53:35 +01:00
Giovanni Bajo 8655d4027f n64: convert remaining CPU branch opcodes to JIT
Convert all remaining integer branch and branch-likely opcodes to direct JIT emission and update decoder JIT flags, while intentionally keeping BC1 on the callf path.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 271e545a49 n64: convert four CPU branches to JIT and silence offsetof warnings
Convert BGTZ, JR, BLTZ, and BGEZ to direct JIT branch emission and update decoder Jit flags accordingly, then extend invalid-offsetof suppression across cpu/recompiler.cpp to match the RSP pattern.
2026-05-11 10:53:35 +01:00
Giovanni Bajo 6b97edc9db n64: convert four more CPU branches to JIT
Convert J, JAL, BNE and BLEZ to direct JIT emission and update decoder metadata so these opcodes no longer follow the callf flush path.
2026-05-11 10:53:35 +01:00
Giovanni Bajo e4f4a7d657 n64: convert BEQ to JIT 2026-05-11 10:53:35 +01:00
Giovanni Bajo 861020eeb7 n64: initial cleanup of CPU JIT, introduce opcode decoder 2026-05-11 10:53:35 +01:00
Dragorn421 0dc20693e2 Remove "eat stdin" behavior on exit 2026-05-06 12:39:56 +01:00
Dragorn421 08746292c7 Fixup nall/nall/gdb/Readme.md (fix #1360) 2026-05-04 13:08:55 +01:00
Dragorn421 e2636505e2 Fixup versionconfig.cmake: pass --no-show-signature to the git command for getting the commit date 2026-04-30 10:40:59 +01:00
Dragorn421 2219f48fca emux v1.1: remove dashes from instruction names (#2518)
New emux spec location, for reference: https://n64brew.dev/wiki/Emux
2026-04-29 10:31:26 +02:00
Luke Usher 91c64bf350 mia: refactor identify to support when multiple systems use the same file extension
Update desktop-ui so that it prompts the user for the desired system if multiple results were returned, ignoring hidden systems.
2026-04-28 16:23:02 +01:00
kev4cards 6f6786e04f n64: add comment to fastSUV use with respect to SPV 2026-04-27 18:47:08 +01:00
remutro 7974cabb43 Minor Qt6 on Linux cleanup 2026-04-27 18:45:33 +01:00
remutro a8ae8fe83f Add ISViewer RAM to save states 2026-04-23 21:06:11 +01:00
remutro e7cdf9c046 Update dependencies (post-v147-2) 2026-04-23 18:19:26 +01:00
Giovanni Bajo 93c397dca7 n64: bump serializer 2026-04-23 18:19:14 +01:00
Giovanni Bajo c9f2773685 n64: silence invalid offsetof warnings 2026-04-23 18:19:14 +01:00
Giovanni Bajo 652d653736 n64: RSP JIT cleanup and add emux tracing support
Verbose refactoring to only use decoder.cpp for extracting information
from opcodes (removing isTerminal and the return value of emit functions).

Added tracing to the cache index so that we basically version the JIT
blocks depending on whether it is enabled or not. This allows to
implement support for emux tracing without overhead.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 219dff38ad n64: refactor load/store opcodes to make better use of constant addresses 2026-04-23 18:19:14 +01:00
Giovanni Bajo c5ca614492 n64: add LRV/SRV/SWV to RSP JIT 2026-04-23 18:19:14 +01:00
Giovanni Bajo 0fca58b98e n64: add LTV/STV to RSP JIT 2026-04-23 18:19:14 +01:00
Giovanni Bajo 5d04cd3808 n64: add RSP JIT for LFV/SFV 2026-04-23 18:19:14 +01:00
Giovanni Bajo 59a0a898e9 n64: add LHV/SHV in RSP JIT, and refactor LUV/LPV/SUV/SPV to always use SIMD 2026-04-23 18:19:14 +01:00
Giovanni Bajo a6499c3b49 nall: fix build on msvc and GCC 13 2026-04-23 18:19:14 +01:00
Giovanni Bajo 310b665160 n64: add LDV/SDV RSP JIT opcodes 2026-04-23 18:19:14 +01:00
Giovanni Bajo b96b8ac7da n64: add LSB/LSV/LLV/SSB/SSV/SLV RSP JIT opcodes 2026-04-23 18:19:14 +01:00
Giovanni Bajo d20a0e1b8e n64: add LUV/LPV/SUV/SPV RSP JIT opcodes 2026-04-23 18:19:14 +01:00
Giovanni Bajo 716d417359 n64: use sreg(3) in RSP JIT to hold the DMEM pointer 2026-04-23 18:19:14 +01:00
Giovanni Bajo 1b6893ea49 nall: allow a recompiler to define a different number of saved registers 2026-04-23 18:19:14 +01:00
Giovanni Bajo fef2ab4904 n64: in RSP, add JIT for lqv/sqv
This generates optimized codepaths for lqv and sqv. It also relies on
constant tracking to optimize situations where the base address is
know at JIT time (very common) and we can thus prove its alignment.
2026-04-23 18:19:14 +01:00
Giovanni Bajo cbf9f79e5f n64: add constant tracking in RSP JIT
The JIT now tracks constant registers within each recompiled blocks.
This is currently unused in the generated code, will be useful to
improve code generation for vector load/stores opcodes.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 4c4c5460bc nall: add support for register arguments to callf
This is non trivial because input arguments might mistakenly override
ABI registers. Prevent register argument clobber by resolving move
dependencies and breaking cycles with a temporary register.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 26d5ac3a80 n64: specialize R0 source propagation in RSP JIT
Fold R0-based scalar and branch fast paths into direct immediates/moves so the emitter skips unnecessary register reads and compares.
2026-04-23 18:19:14 +01:00
Giovanni Bajo f71568bad4 n64: annotate RSP op metadata for JIT state handling
Move UsesDmem and MayHalt traits into OpInfo so decoder metadata directly drives recompiler decisions, and simplify the JIT loop by unifying delay-slot state while documenting the current architecture and optimizations.
2026-04-23 18:19:14 +01:00
Giovanni Bajo a4ac042f27 n64: skip R0 writes in RSP JIT paths
Drop the JIT write-sink indirection and skip code emission when the destination is R0, while keeping side-effecting C++ helpers and guarding their writeback to preserve R0 semantics.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 25e248a6c3 n64: centralize RSP JIT misaligned access capability
Define a single architecture capability macro for unaligned scalar memory access and use constexpr slowpath selection helpers for half/word DMEM ops.
2026-04-23 18:19:14 +01:00
Giovanni Bajo f43063803b n64: gate RSP halted checks to relevant opcodes
Emit status.halted exit checks only for instructions that can mutate halt state (BREAK and MTC0 to SP_STATUS), reducing per-opcode overhead in the main JIT loop.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 9e20c21b35 n64: avoid VU preflush in RSP callf gate
Do not classify VU opcodes as callf preflush candidates so deferred clocks are only synchronized for paths that actually require clock-visible C++ calls.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 44ab78ade5 n64: defer RSP JIT clock flushes to slow paths
Track deferred clock progress for slow-path entries so memory fast paths avoid callf-triggered flushes while preserving correct timing when deferred fallbacks execute.
2026-04-23 18:19:14 +01:00
Giovanni Bajo c88d2eba79 n64: JIT scalar RSP DMEM ops with deferred fallbacks
Emit direct fast paths for LB/LBU/LH/LHU/LWU/SB/SH/SW and keep unaligned half/word cases in deferred slow paths, reducing callf overhead while preserving interpreter behavior.
2026-04-23 18:19:14 +01:00
Giovanni Bajo b1b51d3070 n64: re-emit RSP JIT slow paths via emitEXECUTE
Keep deferred slow-path records minimal and re-enter opcode emission in slow mode, reducing per-opcode slow-path boilerplate while preserving the compact LW fast path.
2026-04-23 18:19:14 +01:00
Giovanni Bajo dc8ac7d3dc n64: switch RSP DMEM accesses to big-endian semantics
Implement DMEM read/write paths with MSB-first word semantics and byte-accurate unaligned wrapping. Adjust RSP DMA DMEM transfers to use word paths consistent with the new DMEM representation while preserving IMEM behavior.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 3feb849778 n64: call branch epilogue directly in RSP delay slots
Replace the delay-slot call to instructionEpilogue<1> with explicit clock flush plus instructionBranchEpilogue, removing wrapper overhead while preserving delay-slot branch semantics.
2026-04-23 18:19:14 +01:00
Giovanni Bajo bc1009a5f1 n64: inline RSP JIT step clock update
Replace the JIT call to RSP::step with a direct thread clock increment while keeping deferred clock flushing behavior, reducing hot-path call overhead without changing interpreter behavior.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 1f06bbc156 n64: batch fixed RSP JIT clocks between call sites
Defer fixed clock stepping across pure inlined opcodes and flush before callf paths, delay-slot handling, halts, and block exit to preserve observable timing while reducing hot-path step calls.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 09bb1cbf3e n64: fold RSP conditional branch state masking writes
Emit the DelaySlot|EndBlock mask directly into branch state destinations for conditional branches, removing temporary state move instructions in the hot path.
2026-04-23 18:19:14 +01:00
Giovanni Bajo b66752b4fc n64: write RSP branch state directly outside delay slots
Emit branch state updates directly in non-delay-slot branch paths and avoid copying nstate into state in the non-delay-slot epilogue, reducing branch-state traffic in the hot path.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 97b9490845 n64: route RSP JIT branch targets by delay-slot phase
Pass delay-slot phase into EXECUTE/SPECIAL/REGIMM emitters so branch-like ops can write the active destination directly and remove redundant delay-slot branch.begin state traffic.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 6e7cf80f6e n64: virtualize RSP JIT block PC progression
Use compile-time next-PC values for non-delay-slot commits and avoid per-op branch.begin updates outside delay slots, reducing hot-path branch state traffic while preserving delay-slot behavior.
2026-04-23 18:19:14 +01:00
Giovanni Bajo d250931aae n64: simplify RSP JIT conditional target selection
Use a compact xor-mask select for conditional branch target/fallthrough selection, reducing generated hot-path instructions while preserving existing branch semantics.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 7163fb4dbb n64: use virtual fallthrough PC in RSP conditional branches
Replace runtime fallthrough reads from branch.nextpc with compile-time PC+8 immediates for REGIMM and EXECUTE conditional branches to remove extra hot-path memory dependencies.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 838f4c6ad2 n64: stabilize RSP virtual-PC branch targets in JIT
Include block base address in the RSP JIT hash and use compile-time branch targets for conditional branches so generated code stays correct while removing extra runtime target math.
2026-04-23 18:19:14 +01:00
Giovanni Bajo c5e5c38a8a n64: use compile-time PC for RSP link writes
Pass the current block PC into RSP JIT emitters and use compile-time PC+8 immediates for JAL/JALR/BLTZAL/BGEZAL link register writes while keeping existing branch flow unchanged.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 9f98c24394 n64: trim RSP JIT non-branch epilogue overhead
Skip branch state/pc commit work for non-branch non-terminal opcodes and simplify branch begin emission, and fix BLTZAL/BGEZAL when Rs aliases RA by deferring link write until after condition evaluation.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 86765b1706 n64: inline RSP JIT conditional branch opcodes
Emit BEQ/BNE/BLEZ/BGTZ and REGIMM conditional branches directly in JIT to remove per-opcode C++ calls while preserving delay-slot and link-register behavior.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 2585b4667f n64: inline simple RSP JIT jump opcodes
Emit J/JAL/JR/JALR branch updates directly in JIT so these terminal opcodes avoid C++ calls while preserving current delay-slot behavior.
2026-04-23 18:19:14 +01:00
Giovanni Bajo bc4332bbb9 n64: remove temporary RSP epilogue A/B switch
Drop the fastInstructionEpilogue runtime toggle now that the specialized JIT epilogue path is stable. Keep the delay-slot fallback path while making the optimized path the only code path.
2026-04-23 18:19:14 +01:00
Giovanni Bajo a3d75ad5da n64: specialize RSP JIT epilogue by delay-slot position
Choose the fast epilogue path at compile time for non-delay-slot instructions and keep the legacy path only for delay slots. This removes per-opcode branch-state dispatch in the hot path while preserving behavior.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 101ba34f28 n64: inline RSP epilogue slow-path clock accounting
Replace the slow-path JIT epilogue call with direct step/clocksTotal updates and a call to instructionBranchEpilogue, keeping branch exit semantics unchanged. This reduces call overhead while preserving behavior under the fast epilogue toggle.
2026-04-23 18:19:14 +01:00
Giovanni Bajo ece2001360 n64: widen RSP program counter storage
Store RSP IPU PC as 32-bit so JIT word-sized accesses cannot clobber adjacent state. This keeps the fast epilogue path behaviorally stable while preserving 12-bit address masking semantics.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 3e913fda53 n64: add toggled fast RSP instruction epilogue path
Add an A/B switch for a fast JIT epilogue path while preserving the legacy path as fallback, and widen branch pc storage so JIT word-sized accesses are safe. This gives a stable way to compare behavior and performance during further tuning.
2026-04-23 18:19:14 +01:00
Giovanni Bajo ec29392c11 n64: align RSP branch state with CPU pipeline model
Model RSP branch flow with pc/nextpc and latched state/nstate so interpreter and JIT follow the same delay-slot and end-block semantics. This stabilizes branch behavior while keeping stall and dual-issue logic unchanged.
2026-04-23 18:19:14 +01:00
Giovanni Bajo 536fe6964f n64: factor RSP branch epilogue handling
Extract branch-state commit logic into a dedicated helper so we can iterate on JIT epilogue changes from a stable, behavior-preserving baseline.
2026-04-23 18:19:14 +01:00
remutro 18be35d9e0 Fix incorrect setting 2026-04-23 12:49:58 +01:00
remutro c6596bc468 Fix PlayStation extensions 2026-04-23 12:49:58 +01:00
remutro 00bdc72562 Fix MMI extension loading and detection 2026-04-23 12:49:58 +01:00
NovaSquirrel 0394fd90ad snes: update SerializerVersion to v145 2026-04-18 13:47:54 +01:00
NovaSquirrel f27d62f841 snes: fix SGB register mirroring to match hardware
According to fullsnes, SGB only decodes A0-A3,A11-A15,A22
2026-04-18 13:47:54 +01:00
NovaSquirrel 874d32eb42 snes: fix SGB logic for moving to next player
This is supposed to fix #1205
2026-04-18 13:47:54 +01:00
NovaSquirrel 8e10f41188 snes: Fix Super Game Boy $6002 and $700x registers
The Super Game Boy only stores one packet, and packet acknowledgement is done by reading $7000, not $6002
2026-04-18 13:47:54 +01:00
ZengGengSen ad98f53f3b refactor: move reset state from MOS6502 to system-level CPU
Refactor reset handling to align with project architecture by removing the
'resetting' flag from the MOS6502 core. The reset state is now managed via
'resetPending' within the specific A2600 and Famicom CPU implementations.

- MOS6502: Remove resetting flag and internal reset logic from power().
- CPU: Introduce io.resetPending to handle reset triggers in the main loop.
- Debugger: Add interrupt/reset notification support for better tracing.
- Serialization: Update A26 (v140) and FC (v152) versions for state changes.
2026-04-09 13:01:38 +01:00
ZengGengSen 6e0fa73249 fc: add support for Dendy region
- Implement Dendy-specific timings: CPU divider 15, 312 total scanlines, and vblank starting at scanline 291.
- Refactor CPU/APU rate calculation to use a dynamic system divider instead of hardcoded PAL/NTSC checks.
- Add Dendy to the system region enumeration and UI emulator list.
- Handle Dendy-specific hardware differences: disable microphone input and adjust PPU aspect ratio/pixel output offset to match PAL-like characteristics.
- Clean up controller and system unloading logic to prevent memory leaks.
2026-04-08 09:12:51 +01:00
User 722f104fa0 gba: allow re-triggering keypad IRQ when input changes 2026-04-08 09:09:05 +01:00
Giovanni Bajo d55f80d633 n64: in emux, implement xioctl fast/slow
These can be used by testroms to automatically turn on fast forward mode
2026-04-07 08:53:16 +01:00
User 649a29debe gba: fix wave RAM bank selection 2026-04-04 08:56:02 +01:00
Giovanni Bajo dedc446418 n64: in emux, add detect mask for xioctl 2026-04-02 09:35:33 +01:00
Giovanni Bajo 16c5da5dc5 n64: implement emux on RSP
This implements a first subset of the emux extensions on the RSP. The
implementation is similar to the CPU one, and forwards to it things
that have to be identical.
2026-03-31 09:12:22 +01:00
Giovanni Bajo a0f762985e n64: add initial emux support
Emux is a spec for emulator extensions to help homebrew development.
The spec can be found here at the time of writing:
https://hackmd.io/@rasky/r1k7na6Jn

Usage of emux is gated behind the "homebrew development mode" setting.

This commit introduces support for several extensions on the CPU, but
none on the RSP yet.
2026-03-31 09:12:22 +01:00
Giovanni Bajo f76dd51134 n64: introduce RBusDevice for tracking devices accessing the RAM bus
For homebrew profiling features, we need to track what hardware device
is accessing the shared RDRAM. We do have this already for debugging
purposes as a string. This commit changes that to an enum.
2026-03-31 09:12:22 +01:00
Giovanni Bajo 103dd93ae4 ares: add an event to allow a core to request emulator shutdown
Normally, cores shouldn't be able to request the emulator to shut down
on the host computer, but this might be required for some homebrew
development features.
2026-03-31 09:12:22 +01:00
remutro 5f216a7bf5 Add Sega PAC firmware hashes for auto detection 2026-03-30 10:09:08 +01:00
User dfa6ec0c79 gba: improve affine parameter timings 2026-03-26 10:50:22 +00:00
User e011cbecbb gba: window accuracy improvements 2026-03-20 11:20:42 +00:00
Scott Mansell c2b349aeb8 cmake: Add missing include sources 2026-03-19 08:27:52 +00:00
remutro 6f0f7be867 Increase PCE Serialization Version 2026-03-18 14:57:43 +00:00
awsms bc86249658 desktop-ui: fix kiosk crash, ignore hotkeys while Tools window is focused 2026-03-18 12:19:06 +00:00
ZengGengSen 0ac4d8220c cmake: fix XAudio2 detection logic for MSVC vs non-MSVC toolchains
Previously the condition checked for MINGW, treating MSVC and all other
toolchains as a single "no detection needed" case. This inverts the
guard to MSVC, so that MSVC correctly skips detection (Windows SDK
provides xaudio2 directly) while MinGW and other non-MSVC toolchains
go through the CheckCXXSourceCompiles probe as intended.
2026-03-17 16:11:06 +00:00
ZengGengSen 1ce484be52 cmake: add FindXAudio2 module and fix debug symbol detection on MinGW
Add cmake/finders/FindXAudio2.cmake to replace the ad-hoc XAudio2
link logic in os-windows.cmake. On MinGW, the module uses
check_cxx_source_compiles to probe for xaudio2_9 and xaudio2_8 in
the sysroot; on MSVC and other toolchains the Windows SDK umbrella
library is used directly. An imported target XAudio2::XAudio2 is
exported so call sites need no platform conditionals.

Replace the generator-expression-based -gcodeview selection in
compilerconfig.cmake with an explicit check_cxx_compiler_flag probe
at configure time (ARES_COMPILER_SUPPORTS_GCODEVIEW). This avoids
passing -gcodeview to toolchains that do not support it and makes
the fallback to DWARF visible in the configure log rather than
silently resolved at build time.

- ruby/cmake/os-windows.cmake: call find_package(XAudio2) and
  consume XAudio2::XAudio2 via a generator expression; remove the
  old MSVC/else block that unconditionally linked xaudio2/xaudio2_9
- cmake/windows/compilerconfig.cmake: add -gcodeview capability
  probe; rewrite Clang and GCC debug-symbol blocks to branch on the
  cached result instead of a generator expression
2026-03-17 16:11:06 +00:00
awsms 899ac2a2ed desktop-ui: prevent hotkey from firing when setting one 2026-03-17 16:09:33 +00:00
Florian-Simon-RedArtGames ca6974b66f Fix glitch in City Hunter ROM in the intro in the performance-mode VDP
We were rendering background and sprites in burst mode, which shouldn't
happen.

I'm not the author, I'm just upstreaming changes done a member in our
team.
2026-03-17 16:08:37 +00:00
remutro 3d966bda8f Updated n64 serilization 2026-03-16 17:25:29 +00:00
remutro b11ee3fbdb Update serialized fields in N64 save states 2026-03-16 17:25:29 +00:00
User 3f8b0cae72 gba: slightly improve PPU timings 2026-03-16 17:25:20 +00:00
User 8261563a10 gba: fix mid-scanline toggling of background mosaic 2026-03-16 17:25:20 +00:00
remutro b15ce04e5d XAudio2 latency adjustment 2026-03-10 10:52:42 +00:00
ZengGengSen 7a515cd489 feat(fc): add support for JY Company and SC-127 boards
- Implementation of JY Company (Mapper 90, 209, 211) with support for
  complex PRG/CHR banking, IRQ counter modes, and math registers.
- Implementation of SC-127 (Wario Land II Kirby hack) with A12-based
  IRQ filtering.
- Enhanced CPU with `isWriting()` check and PPU with `isRenderingRead()`
  status to support advanced mapper timing.
- Updated MIA database to recognize iNES mappers 35, 90, 209, and 211.
2026-03-10 10:51:44 +00:00
Stefan Schlosser 5b7dafe70a CI: Update FreeBSD runner to 15.0
Update cross-platform-actions to v0.32.0 by using the explicit commit
https://github.com/cross-platform-actions/action/commit/492b0c80085400348c599edace11141a4ee73524

The change from 14.3 to a string literal '15.0' is mandatory, otherwise
the FreeBSD VM image cannot be fetched.
2026-03-08 16:35:08 +00:00
Luke Usher eaa47fbf9c a26: improve RIOT emulation, fixes Berzerk (likely others) 2026-03-06 21:14:18 +00:00
remutro bbc0c1db3b [PS1] Update state serialization 2026-03-06 18:57:35 +00:00
Giovanni Bajo b35a639e33 n64: adjust eeprom timings
Further hardware tests confirmed the EEPROMs are faster than initially
thought.
2026-03-06 15:23:54 +00:00
Giovanni Bajo 48b8c8a3ff n64: implement eeprom write timing and status flags
Currently Ares implements an infinitely fast EEPROM, but alas real
EEPROMs can take even 20 ms to save a single block of 8 bytes.

This commit implement approximate timings and the related flag to
notify the CPU that the EEPROM is busy writing data.
2026-03-06 10:07:41 +00:00
awsms 5a80359443 ruby(audio): enable SDL_HINT_NO_SIGNAL_HANDLERS to avoid ctrl+C hijacks 2026-03-06 10:07:23 +00:00
awsms 67163a9fbe ruby: enable SDL_HINT_NO_SIGNAL_HANDLERS to avoid ctrl+C hijacks 2026-03-05 13:56:48 +00:00
remutro 0d1c90c616 Remove OpenGL macOS Video Driver 2026-03-04 13:06:56 +00:00
awsms 3678a4cc07 Introducing --kiosk mode (#2433)
Introduce a `--batch` command-line option to run with a minimal presentation for frontend-driven launches (eg EmulationStation, arcade cabinets etc).

Behavior
- `--batch` implies `--no-file-prompt`
- Requires at least one valid game path (otherwise exits with a clear error)
- Uses a minimal presentation:
  - hides menu bar
  - removes status bar
  - black background / centered viewport
  - keeps file-open and drag/drop support
- Skips constructing settings/tools/game-browser windows in batch

Runtime safety
- Batch mode avoids UI-only flows where possible (logs messages instead)
- Driver and load/identify failures trigger a clean batch exit path
- Runtime defocus checks now use `settings.input.defocus` instead of `driverSettings.*.checked()`:
  - avoids dereferencing non-constructed UI widgets in batch
  - fixes batch crash from UI-state reads on non-UI code paths
2026-03-04 13:06:36 +00:00
Luke Usher c0a0f34439 ps1: fix some gpu clocking issues 2026-03-01 04:11:18 +00:00
Luke Usher 640ea5e3fa ps1: fix spu canReadDMA/canWriteDMA flags 2026-02-28 23:32:36 +00:00
Luke Usher cb40c6487b ps1: fix block dma using incorrect timing 2026-02-28 23:32:16 +00:00
Luke Usher 1570af44e2 ps1: fix incorrect bit position for dma channel chopping enable 2026-02-28 23:31:52 +00:00
Luke Usher d00c9feeac ps1: fix an issue where configured io access timing for exp1, exp3, bios, cpu, cdrom and exp2 would not apply 2026-02-28 23:30:24 +00:00
Luke Usher b308958bf0 cdrom: refactor CD-ROM session generation and image file handling
- Stop mixing time/address bases; we inconsistently treat LBA 0 as both 00:00:00 and 00:02:00.
 - Clearly separate logical and physical domains:
    - MSF: The MSF implementation assumes absolute MSF (lba 0 = 00:02:00)
    - LBA: logical addressing  (LBA 0 = Track 1 Index 01 / 00:02:00)
    - ABA: absolute addressing (ABA 0 = Track 1 Index 00 / 00:00:00)
 - Fix subchannel generation and support negative MSF (90–99)
 - Fix discs with non-default track 01 index 01 lba
2026-02-28 21:27:54 +00:00
Luke Usher 9125d0c79f ps1: rework cd-rom emulation 2026-02-28 21:27:53 +00:00
User ee80782345 gba: refactor bus reads 2026-02-28 09:45:06 +00:00
meauxdal b5fd6611f2 Update serialization.cpp 2026-02-27 19:55:12 +00:00
Luke Usher 12558c7ef3 gpu: fix hblank timing 2026-02-24 20:54:53 +00:00
awsms 99733cda7c fix abort on exit caused by invalid mutex unlock
`Program::quit()` could call `unlock()` on a `unique_lock` that did not own
the mutex, triggering `std::system_error` ("Operation not permitted")
and terminating the process on exit.

Guard the `unlock()` call with `owns_lock()`.
2026-02-24 12:03:13 +00:00
awsms 1482689f09 ps1: enable scheduler synchronization
Call scheduler.setSynchronize(synchronize) during PS1 serialization
to match the behavior used by other cores.

Without synchronized serialization, coroutine/thread state may be
captured inconsistently, which can lead to crashes or undefined
behavior when loading savestates after relaunch.

Bump serializer version to v147 to invalidate incompatible v146
savestates created without scheduler synchronization.
2026-02-24 12:02:43 +00:00
Alex Bates a1d3fc19a2 n64: fix return GDB read result for RCP/PI bus path
The multi-byte read handler for the RCP/PI address range
(0xA4000000-0xBFFFFFFF) correctly fills the result buffer via
bus.read<Word> but then falls through to `return {}` instead
of returning the result. This causes GDB memory reads from ROM
cartridge addresses to return empty responses.
2026-02-23 19:15:23 +00:00
User 103770a995 gba: prevent instruction tracing from modifying open bus 2026-02-21 08:25:49 +00:00
Le-Syl21andClaude Opus 4.6 38a2e62603 Fix Linux shader install: copy contents instead of directory
CMake install(DIRECTORY "path") without a trailing slash copies the
directory itself, creating an extra shaders_slang/ level under Shaders/.

Adding a trailing slash makes it copy the contents directly, consistent
with macOS (ditto) and Windows (copy_directory) which both copy contents.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-16 08:22:39 +00:00
meauxdal ae41a43567 n64: fix audio pops when AI DMA ends
On a real console, when the AI DMA ends, even if the volume is not zero,
there is a decay happening on the analog path that doesn't cause any
audible pops. This can happen in games when they fail to meet the
audio deadline and schedule a new buffer before the previous one ends.
2026-02-15 22:53:40 +01:00
TascoDLX 08a7253e99 md: fix screen in interlace mode 1 2026-02-13 13:30:28 +00:00
remutro ba6069686f add missing file 2026-02-08 17:32:01 +00:00
remutro 6bb521a207 Import & export settings in the UI 2026-02-08 17:32:01 +00:00
remutro fac53a08ca Windows OpenGL 4.6 Core Profile Driver 2026-02-08 17:31:32 +00:00
izzy d67a92b2b4 make lambda more concise and strict 2026-02-08 17:31:12 +00:00
remutro ffee6d658d Fix fast forward getting stuck 2026-02-03 17:23:58 +00:00
remutro 58f3d7badf Exclude additional systems from using Run-Ahead 2026-02-03 17:23:36 +00:00
remutro 388e98fba9 Fix bug with disabling of secondary media 2026-01-30 06:40:08 +00:00
remutro f883420714 sort emulator cores by manufacturer 2026-01-30 06:40:08 +00:00
remutro d99f7598e1 macOS looks better using same value as Windows 2026-01-30 06:40:08 +00:00
remutro 64bfe58e03 Cleanup UI text and controls 2026-01-30 06:40:08 +00:00
meauxdal d05f7b64b4 Update vulkan.cpp 2026-01-28 07:55:22 +00:00
User bc036dcaf6 gba: improve handling of OBJ mosaic counters 2026-01-28 07:53:43 +00:00
User 64cebdad77 gba: fix interaction between OBJ mosaic and priority 2026-01-28 07:53:43 +00:00
remutro e594251208 add version string to end of help message 2026-01-27 10:12:38 +00:00
remutro cea26bd150 add option to print version string 2026-01-27 10:12:38 +00:00
remutro 27905a8fc7 Add ability to change disc for ldrom2 titles 2026-01-27 10:12:25 +00:00
remutro 2f2704b2b6 adjust string placement 2026-01-27 10:10:10 +00:00
remutro 5afa255ab8 logo alignment in about 2026-01-27 10:10:10 +00:00
remutro daa781706a Use lambda for sort comparer 2026-01-20 11:54:06 +00:00
remutro 88bd798427 Sort once when vector is created 2026-01-20 11:54:06 +00:00
remutro d77c8b1c56 Sort the emulators setting list also 2026-01-20 11:54:06 +00:00
remutro e07f18c172 Sort the emulator list in firmware settings 2026-01-20 11:54:06 +00:00
remutro 6bc13e8d66 Fix crash opening about window 2026-01-20 10:37:42 +00:00
remutro 25482bc539 Single xaudio2 source file, fix vector calls in asio.cpp 2026-01-19 14:34:34 +00:00
remutro 847e2a24cf Fix silly mistake 2026-01-19 14:34:34 +00:00
remutro 514933d459 Fix MSVC Linking 2026-01-19 14:34:34 +00:00
remutro 04e4f0ffa0 Fix MSVC builds 2026-01-19 14:34:34 +00:00
remutro 109c669056 Update XAudio2 from 2.1 to 2.9 2026-01-19 14:34:34 +00:00
Giovanni Bajo fb68c74143 n64: with homebrew header, don't default to install a controller pak
When the homebrew header was first introduced, in its first limited
spec, Ares defaulted to enable both rumble pak and controller pak:
https://github.com/ares-emulator/ares/commit/bc8c308ee96162f5a7068001a0ece83e5f821e16

Back at the time, there wasn't any way to specify that a controller pak
was needed by the ROM, and Ares didn't even provide a GUI to install it.
So basically this was the only way to allow a homebrew game to use
the controller pak at all.

Fast forward 4 years and now we have a GUI to select which pak to install
on each controller, and the homebrew header allows to pre-install whatever
you want into each port.

In case the user doesn't specify a suggested controller configuration
in the homebrew header, the default of using a controller pak has the
annoying side effect of creating a useless .sav file next to any ROM.
CPak is not very common in homebrew ROMs anyway, so enabling it by
default now seems to create more hassle than it solves.
2026-01-19 14:34:02 +00:00
remutro acdf31b40a Stay in pseudo-fullscreen when game is reset 2026-01-18 00:04:56 +00:00
remutro 48c611c90b Fix optnone attribute warning 2026-01-15 12:55:37 +00:00
remutro f338f50b4d some refactoring 2026-01-14 11:36:53 +00:00
remutro f0b119d90a Fix mapped drives not working after symlink fix (Windows) 2026-01-14 11:36:53 +00:00
remutro e8c268026c Update current save slot on successful save state load 2026-01-14 11:33:30 +00:00
remutro 9387eab814 Add command line option to specify a save state slot to load 2026-01-14 11:33:30 +00:00
remutro d769abafd9 Update hotkey text 2026-01-14 11:33:06 +00:00
remutro 742ab90d21 rogue spaces removed 2026-01-14 11:33:06 +00:00
remutro 2a346af4e2 Add hotkey to disable/enable selected shader 2026-01-14 11:33:06 +00:00
Giovanni Bajo df5ebc9245 n64: implement support for region-free ROMs (homebrew extension)
The advanced homebrew header (https://n64brew.dev/wiki/ROM_Header)
specifies a bit to mark a ROM as "region free", meaning that it can be
booted in all regions. Ares now has a region preference option, so
that option can be respected for the region free ROMs.
2026-01-11 17:04:57 +00:00
Luke Usher 71f17d9a2a ps1: fix crash when saving/loading state 2026-01-09 21:00:32 +00:00
Luke Usher 4e7ab5d3e3 ps1: refactor core to use libco threading
- cpu: implemented memory timing/control registers
 - cpu: update bus accesses to obey memory timing registers
 - cdrom: incorrect 150lba offset in command 0x14
 - cdrom: fix behavior when a command is issued before the previous has completed
 - cdrom: implement behavior where a new command cannot be executed until pending IRQ are acknowledged
 - cdrom: implement 'command/parameter busy' register
 - dma: update bus accesses to obey memory timing registers
 - dma: improve chopping/allowing the cpu to run between blocks of a long transfer
 - mdec: implement basic decode timing
 - mdec: implement correct input fifo behavior
 - mdec: fix behavior when an incomplete block is transferred via the input fifo
 - mdec: fix dma request registers, input/output full/empty registers, busy register
 - gpu: use correct clock frequency for PAL and NTSC
 - timer: improve behavior around timer overflow/target reached irq
2026-01-09 16:11:58 +00:00
remutro e5ffe61a07 Fix Dip Switches menu on Linux 2026-01-09 12:53:53 +00:00
remutro e601bccdfb Add pseduefullscreen command line argument 2026-01-09 10:27:41 +00:00
remutro 155c9b4154 Fix formatting 2026-01-09 10:27:31 +00:00
remutro 7b0e29baa9 Expose option to not prompt for secondary media in GUI 2026-01-09 10:27:31 +00:00
jsgroth 0152831319 m32x: adjust H32 pixel offset between Gen/MD VDP and 32X VDP
the adjusted offset is equal to 2.6 H32 pixels / 3.25 H40 pixels
2026-01-08 23:32:28 +00:00
remutro 17ce1bbe0f Fix loading shaders from the command line 2026-01-08 14:15:56 +00:00
remutro db953dbc62 minor format fixes 2026-01-08 09:27:05 +00:00
remutro 65ed77ecd7 Fix symlinks to files for rom loading 2026-01-08 09:27:05 +00:00
Luke Usher bdf4dc9ce1 desktop-ui: add '--settings-file' command line parameter 2026-01-07 10:32:43 +00:00
Luke Usher c01f3b885e desktop-ui: do not enable 'Arcade' if no arcade cores are compiled in. 2026-01-07 10:20:51 +00:00
Luke Usher 538078f6d0 ruby: prefer 'metal' video driver as both optimal and safest on macOS. 2026-01-07 10:11:43 +00:00
Luke Usher 2a4be17f56 ruby: remove erroneous include 2026-01-06 23:15:36 +00:00
ZengGengSen 9d647291fa add unload emulator to save the wav file 2026-01-06 16:32:21 +00:00
ZengGengSen 595de2e2aa fix new tape file resize bug 2026-01-06 16:32:21 +00:00
ZengGengSen 778e1f0b76 feat: add famicom data recorder support
1. add famicom data recorder support
2. refactor the tape control
3. fix the wav bug
2026-01-06 16:32:21 +00:00
User b5910c75f5 gba: prefetcher should fail reading across 128KiB boundaries 2026-01-06 16:21:35 +00:00
Luke Usher 23cd8772f5 ruby: allow drivers to set controller names and use deterministic device id in SDL driver 2026-01-06 12:09:44 +00:00
remutro 202db872de Add Cubivore English translation to N64 DB 2026-01-05 09:55:12 +00:00
remutro 934e9d36c3 Update dependencies (post-v147) 2026-01-02 16:21:26 +00:00
remutro 30066aa336 [SFC] Add support for GBC titles to the Super Game Boy 2025-12-31 12:33:22 +00:00
invertego cc4db18707 libco: limit scope of section hack to gcc
When placing co_swap_function byte array into the text section, libco
uses the trick of appending a # to the section name. This name string is
passed directly to the generated assembly and the # is treated as
comment token by the GNU assembler. This is a hack to avoid a warning
about changed section attributes.

This hack doesn't work with other compilers like Clang or TinyCC, but
unlike GCC they don't require such hacks. I've added some compile time
checks around the hack to try to isolate it to GCC and allow other
compilers to see the unmodified section name.

Validated on the following OSes/architectures/compilers:
- Windows/amd64: gcc, clang, cl, clang-cl, tcc
- Windows/aarch64: clang, cl, clang-cl
- Linux/amd64: gcc, clang, tcc[1]
- Linux/aarch64: gcc, clang, tcc[1]
- macOS/aarch64: gcc, clang

[1] TCC only works with a fix to glibc headers that was only made very
recently. Without it, you have to #undef __attribute__ after including
glibc headers.
2025-12-29 12:01:33 +00:00
invertego 30a3b125c8 libco: align co_active_buffer on amd64, aarch64
This is required on amd64 due to the aligned moves in co_swap. While
note technically needed on aarch64, I found information online that
suggested ldp/stp perform better on certain microarchitectures with
16-byte alignment.
2025-12-29 12:01:33 +00:00
User 084d8fb72b gba: only trigger keypad IRQ when input changes 2025-12-29 12:01:15 +00:00
User 463d44920b gba: fix handling of repeat bit during immediate DMA transfers 2025-12-27 22:38:59 +00:00
Stefan Schlosser adfc78f952 pce: fix missing qon linking 2025-12-23 14:44:52 +00:00
1971 changed files with 251978 additions and 312253 deletions

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ set(.github_sources
ISSUE_TEMPLATE/feature_request.md
ISSUE_TEMPLATE/game-issue-report.md
scripts/build_macos.sh
scripts/build_sdl.sh
scripts/build_ubuntu.sh
scripts/build_windows.sh
scripts/package_artifacts.sh
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# clone the SDL repository
git clone https://github.com/libsdl-org/SDL.git
# change into the SDL directory
pushd SDL
mkdir build
# build SDL
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
# install SDL libs and include files to /usr/local
sudo cmake --install build --prefix=/usr/local
popd
+25 -15
View File
@@ -17,11 +17,13 @@ jobs:
msystem: mingw64
install: make git cmake ccache ninja mingw-w64-x86_64-toolchain
target-cmake-preset: windows-ci-mingw-native
- name: windows-clang-cl-x64
- name: windows-mingw-clang64-x64
os: windows-latest
windres: rc
shell: bash
target-cmake-preset: windows-ci-cl-native
compiler: clang++
shell: 'msys2 {0}'
msystem: clang64
install: make git cmake ccache ninja mingw-w64-clang-x86_64-clang
target-cmake-preset: windows-ci-mingw-native
- name: windows-arm64
os: windows-11-arm
compiler: clang++
@@ -62,7 +64,7 @@ jobs:
target-cmake-preset: ubuntu-ci-qt
- name: freebsd
os: ubuntu-24.04
shell: bash
shell: cpa.sh {0}
target-cmake-preset: ubuntu-ci-clang
name: ${{ matrix.program }}-${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
@@ -89,7 +91,10 @@ jobs:
if: runner.os == 'Linux' && matrix.platform.name != 'freebsd'
run: |
sudo apt-get update -y -qq
sudo apt-get install cmake ccache ninja-build libsdl2-dev libgtk-3-dev libao-dev libopenal-dev qt6-base-dev
sudo apt-get install cmake ccache ninja-build libsdl2-dev libgtk-3-dev qt6-base-dev
- name: Build SDL3
if: runner.os == 'Linux' && matrix.platform.name != 'freebsd'
run: .github/scripts/build_sdl.sh
- name: "Build: Linux"
if: runner.os == 'Linux' && matrix.platform.name != 'freebsd'
run: .github/scripts/build_ubuntu.sh
@@ -103,18 +108,23 @@ jobs:
CROSS_COMPILE: ${{ matrix.platform.native-cmake-preset != '' }}
NATIVE_PRESET: ${{ matrix.platform.native-cmake-preset }}
TARGET_PRESET: ${{ matrix.platform.target-cmake-preset }}
- name: "Build: FreeBSD"
- name: "Start FreeBSD VM"
if: matrix.platform.name == 'freebsd'
uses: cross-platform-actions/action@e8a7b572196ff79ded1979dc2bb9ee67d1ddb252
env:
TARGET_PRESET: ${{ matrix.platform.target-cmake-preset }}
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641
with:
operating_system: freebsd
version: 14.3
environment_variables: TARGET_PRESET
run: |
sudo pkg install -y cmake pkgconf ccache ninja gtk3 libX11 libXext libXrandr libXrender libglvnd alsa-lib libao libudev-devd librashader openal-soft pulseaudio sdl3 vulkan-loader
.github/scripts/build_ubuntu.sh
version: '15.1'
- name: "Install FreeBSD Dependencies"
if: matrix.platform.name == 'freebsd'
run: |
sudo pkg upgrade -y
sudo pkg install -y cmake-core pkgconf ccache ninja gtk3 sdl3 librashader
- name: "Build: FreeBSD"
if: matrix.platform.name == 'freebsd'
env:
TARGET_PRESET: ${{ matrix.platform.target-cmake-preset }}
shell: cpa.sh {0} --environment-variables TARGET_PRESET
run: .github/scripts/build_ubuntu.sh
- name: "Compress Build Artifacts (Windows)"
if: runner.os != 'macOS' && runner.os != 'Linux'
run: |
+3 -5
View File
@@ -24,11 +24,9 @@ jobs:
platform:
- name: windows-x64
os: windows-latest
compiler: clang++
shell: 'msys2 {0}'
msystem: clang64
install: make git cmake ccache ninja mingw-w64-clang-x86_64-clang
target-cmake-preset: windows-ci-mingw-native
windres: rc
shell: bash
target-cmake-preset: windows-ci-cl-native
- name: windows-clang-cl-arm64
os: windows-latest
windres: rc
+2
View File
@@ -1,4 +1,6 @@
name: Release
permissions:
contents: write
on:
workflow_call:
inputs:
+3
View File
@@ -41,6 +41,9 @@ desktop-ui/resource/resource.hpp
mia/resource/resource.cpp
mia/resource/resource.hpp
# Exclude Qt generated MOC file
hiro/qt/qt.moc
# Exclude fetched third-party processor test binaries
tests/arm7tdmi/tests
tests/m68000/tests
+4 -3
View File
@@ -18,7 +18,7 @@ add_subdirectory(hiro)
# gersemi: off
set(
ARES_CORES
a26 fc sfc sg ms md ps1 pce ng msx cv myvision gb gba ws ngp spec n64
a26 a52 fc sfc sg ms md ps1 pce ng msx cv myvision gb gba ws ngp spec n64
CACHE STRING "List of systems to be built by the project"
)
# gersemi: on
@@ -30,7 +30,9 @@ add_subdirectory(desktop-ui)
option(ARES_BUILD_OPTIONAL_TARGETS "Include supplemental tools and tests" OFF)
if(ARES_BUILD_OPTIONAL_TARGETS)
enable_testing()
add_subdirectory(tests/arm7tdmi)
add_subdirectory(tests/armv6m)
add_subdirectory(tests/i8080)
add_subdirectory(tests/m68000)
if(HIRO_BACKEND STREQUAL "GTK3")
@@ -38,12 +40,11 @@ if(ARES_BUILD_OPTIONAL_TARGETS)
else()
target_disable_subproject(genius "genius (database editor)")
endif()
add_subdirectory(tools/mame2bml)
else()
target_disable_subproject(arm7tdmi "arm7tdmi processor test harness")
target_disable_subproject(armv6m "armv6m processor test harness")
target_disable_subproject(i8080 "i8080 processor test harness")
target_disable_subproject(m68000 "m68000 processor test harness")
target_disable_subproject(mame2bml "mame2bml (MAME manifest converter)")
target_disable_subproject(genius "genius (database editor)")
endif()
+6 -6
View File
@@ -46,7 +46,7 @@
"rhs": "Windows"
},
"binaryDir": "${sourceDir}/build_vsclang",
"generator": "Visual Studio 17 2022",
"generator": "Visual Studio 18 2026",
"toolset": "ClangCL",
"cacheVariables": {
"ENABLE_CCACHE": true
@@ -63,7 +63,7 @@
"rhs": "Windows"
},
"binaryDir": "${sourceDir}/build_msvc",
"generator": "Visual Studio 17 2022",
"generator": "Visual Studio 18 2026",
"cacheVariables": {
"ENABLE_CCACHE": true
}
@@ -118,7 +118,7 @@
"name": "windows-ci-cl-native",
"displayName": "Windows for CI (x64 ClangCL)",
"description": "Single-arch binary for building on Github Actions",
"generator": "Visual Studio 17 2022",
"generator": "Visual Studio 18 2026",
"inherits": ["windows-ci-base"],
"toolset": "ClangCL"
},
@@ -127,7 +127,7 @@
"displayName": "Windows for CI (arm64 ClangCL)",
"description": "Single-arch binary for building on Github Actions",
"inherits": ["windows-ci-base"],
"generator": "Visual Studio 17 2022",
"generator": "Visual Studio 18 2026",
"architecture": "arm64",
"toolset": "ClangCL",
"cacheVariables": {
@@ -138,14 +138,14 @@
"name": "windows-ci-msvc-native",
"displayName": "Windows for CI (x64 MSVC)",
"description": "Single-arch binary for building on Github Actions",
"generator": "Visual Studio 17 2022",
"generator": "Visual Studio 18 2026",
"inherits": ["windows-ci-base"]
},
{
"name": "windows-ci-msvc-cross",
"displayName": "Windows for CI (arm64 MSVC)",
"description": "Single-arch binary for building on Github Actions",
"generator": "Visual Studio 17 2022",
"generator": "Visual Studio 18 2026",
"architecture": "arm64",
"inherits": ["windows-ci-base"],
"cacheVariables": {
+34
View File
@@ -16,6 +16,40 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
----------------------------------------------------------------------
----------------------------------------------------------------------
MAME-derived ROM decryption and arcade database
Portions of the ROM decryption code are derived from MAME source files
licensed under the BSD 3-Clause License. The arcade database is generated
from MAME machine data.
Copyright (c) 1997-2026 MAMEdev and contributors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
----------------------------------------------------------------------
----------------------------------------------------------------------
Stack-less Just-In-Time compiler
+6
View File
@@ -38,12 +38,18 @@ When started from the command-line, ares accepts a few options.
Usage: ./ares [options] game(s)
--help Displays available options and exit
--version Displays the version string of the application
--terminal Create new terminal window (Windows only)
--fullscreen Start in full screen mode
--pseudofullscreen Start in pseudo full screen mode
--kiosk Start in minimal UI mode (implies --no-file-prompt)
--system system Specify the system name
--shader shader Specify a slang shader to load (requires OpenGL or Metal)
--setting name=value Specify a value for a setting
--dump-all-settings Show a list of all existing settings and exit
--no-file-prompt Do not prompt to load (optional) additional roms (eg: 64DD)
--settings-file path Specify a settings file override (settings.bml)
--save-state slot Specify a save state slot to load (1-9)
```
The --system option is useful when the system type cannot be auto-detected.
+19
View File
@@ -99,6 +99,18 @@ target_sources(
ares/node/video/video.hpp
)
target_sources(
ares
PRIVATE
ares/node/debugger/debugger.hpp
ares/node/debugger/graphics.hpp
ares/node/debugger/memory.hpp
ares/node/debugger/properties.hpp
ares/node/debugger/tracer/tracer.hpp
ares/node/debugger/tracer/instruction.hpp
ares/node/debugger/tracer/notification.hpp
)
if(OS_WINDOWS)
include(cmake/os-windows.cmake)
elseif(OS_MACOS)
@@ -140,6 +152,13 @@ else()
disable_core("Atari 2600")
endif()
if(a52 IN_LIST ARES_CORES)
enable_core("Atari 5200")
add_subdirectory(a52)
else()
disable_core("Atari 5200")
endif()
if(fc IN_LIST ARES_CORES)
enable_core("NES / Famicom")
add_subdirectory(fc)
+77 -2
View File
@@ -1,4 +1,4 @@
ares_components(mos6502)
ares_components(arm7tdmi armv6m m24c mos6502)
ares_add_sources(
CORE #
@@ -10,6 +10,9 @@ ares_add_sources(
controller/controller.cpp
cartridge/cartridge.cpp
cpu/cpu.cpp
elf/elf.cpp
harmony/harmony.cpp
video/video.cpp
tia/tia.cpp
riot/riot.cpp
)
@@ -19,6 +22,7 @@ ares_add_sources(
a26
INCLUDED #
a26.hpp
persistent-memory.hpp
CMakeLists.txt
)
@@ -36,11 +40,48 @@ ares_add_sources(
cartridge/board/board.cpp
cartridge/board/board.hpp
cartridge/board/commavid.cpp
cartridge/board/cdf.cpp
cartridge/board/bus.cpp
cartridge/board/chetiry.cpp
cartridge/board/fa2.cpp
cartridge/board/dpc-plus.cpp
cartridge/board/elf.cpp
cartridge/board/linear.cpp
cartridge/board/movie-cart.cpp
cartridge/board/parker-bros.cpp
cartridge/board/tigervision.cpp
)
ares_add_sources(
CORE #
a26
INCLUDED #
movie-cart/format.hpp
)
ares_add_sources(
CORE #
a26
INCLUDED #
elf/elf.hpp
elf/queue.cpp
elf/parser.cpp
elf/linker.cpp
elf/memory.cpp
elf/vcslib.cpp
elf/runtime.cpp
)
ares_add_sources(
CORE #
a26
INCLUDED #
harmony/harmony.hpp
harmony/memory.cpp
harmony/serialization.cpp
harmony/timing.cpp
)
ares_add_sources(
CORE #
a26
@@ -50,6 +91,25 @@ ares_add_sources(
controller/port.hpp
controller/gamepad/gamepad.cpp
controller/gamepad/gamepad.hpp
controller/booster-grip/booster-grip.cpp
controller/booster-grip/booster-grip.hpp
controller/sega-genesis/sega-genesis.cpp
controller/sega-genesis/sega-genesis.hpp
controller/joy2b-plus/joy2b-plus.cpp
controller/joy2b-plus/joy2b-plus.hpp
controller/savekey/savekey.cpp
controller/savekey/savekey.hpp
controller/atarivox/atarivox.cpp
controller/atarivox/atarivox.hpp
controller/atarivox/speakjet.cpp
controller/atarivox/speakjet.hpp
controller/kidvid/audio.cpp
controller/kidvid/audio.hpp
controller/kidvid/kidvid.cpp
controller/kidvid/kidvid.hpp
controller/kidvid/tables.hpp
controller/quadtari/quadtari.cpp
controller/quadtari/quadtari.hpp
)
ares_add_sources(
@@ -81,18 +141,33 @@ ares_add_sources(
system/system.hpp
)
ares_add_sources(
CORE #
a26
INCLUDED #
video/color.cpp
video/receiver.cpp
video/serialization.cpp
video/video.hpp
)
ares_add_sources(
CORE #
a26
INCLUDED #
tia/audio.cpp
tia/analog.cpp
tia/ball.cpp
tia/color.cpp
tia/collision.cpp
tia/io.cpp
tia/missile.cpp
tia/objects.cpp
tia/player.cpp
tia/playfield.cpp
tia/priority.cpp
tia/serialization.cpp
tia/timing.cpp
tia/tia.hpp
tia/trigger.cpp
tia/write-queue.cpp
)
+3 -1
View File
@@ -1,7 +1,9 @@
#include <a26/cartridge/cartridge.cpp>
#include <a26/controller/controller.cpp>
#include <a26/cpu/cpu.cpp>
#include <a26/elf/elf.cpp>
#include <a26/harmony/harmony.cpp>
#include <a26/video/video.cpp>
#include <a26/riot/riot.cpp>
#include <a26/system/system.cpp>
#include <a26/tia/tia.cpp>
+9 -1
View File
@@ -1,9 +1,13 @@
#pragma once
#include <ares/ares.hpp>
#include <array>
#include <vector>
#include <component/processor/mos6502/mos6502.hpp>
#include <component/processor/arm7tdmi/arm7tdmi.hpp>
#include <component/processor/armv6m/armv6m.hpp>
#include <component/eeprom/m24c/m24c.hpp>
namespace ares::Atari2600 {
#include <ares/inline.hpp>
@@ -16,11 +20,15 @@ namespace ares::Atari2600 {
static inline auto SECAM() -> bool;
};
#include <a26/movie-cart/format.hpp>
#include <a26/persistent-memory.hpp>
#include <a26/controller/controller.hpp>
#include <a26/system/system.hpp>
#include <a26/elf/elf.hpp>
#include <a26/harmony/harmony.hpp>
#include <a26/cartridge/cartridge.hpp>
#include <a26/cpu/cpu.hpp>
#include <a26/video/video.hpp>
#include <a26/tia/tia.hpp>
#include <a26/riot/riot.hpp>
}
+48
View File
@@ -0,0 +1,48 @@
struct Activision8k : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n1 bank;
bool lastAccessWasFE;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto save() -> void override {
}
auto unload() -> void override {
}
auto read(n16 address, n8 data) -> n8 override {
if(address.bit(12)) data = rom.read((bank * 0x1000) + (address & 0xfff));
access(address, data);
return data;
}
auto write(n16 address, n8 data) -> n8 override {
access(address, data);
return data;
}
auto power(bool reset) -> void override {
bank = 0;
lastAccessWasFE = false;
}
auto serialize(serializer& s) -> void override {
s(bank);
s(lastAccessWasFE);
}
private:
auto access(n16 address, n8 data) -> void {
if(lastAccessWasFE) {
//Released 8 KiB FE cartridges ignore D7-D6 and invert D5 to select one of two banks.
bank = data.bit(5) ^ 1;
lastAccessWasFE = false;
return;
}
lastAccessWasFE = address == 0x01fe;
}
};
+47
View File
@@ -0,0 +1,47 @@
struct AmigaFC : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
u32 bankCount = 0;
u8 activeBank = 0;
u8 targetBank = 0;
auto load() -> void override {
if(!Interface::load(rom, "program.rom")) return;
bankCount = rom.size() / 4_KiB;
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address == 0x1ffc) activeBank = targetBank % bankCount;
if(address.bit(12)) return rom.read(activeBank * 0x1000 + (address & 0xfff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address == 0x1ff8) {
targetBank = data & 3;
} else if(address == 0x1ff9) {
auto highBank = (u32)data << 2;
if(highBank < bankCount) {
targetBank += highBank;
targetBank %= bankCount;
} else {
targetBank = data % bankCount;
}
} else if(address == 0x1ffc) {
activeBank = targetBank % bankCount;
}
return data;
}
auto power(bool reset) -> void override {
activeBank = 0;
targetBank = 0;
}
auto serialize(serializer& s) -> void override {
s(activeBank);
s(targetBank);
}
};
@@ -0,0 +1,30 @@
struct Atari32In1 : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n5 game;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
if(address.bit(12)) return rom.read(game * 0x800 + (address & 0x07ff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
return data;
}
auto power(bool reset) -> void override {
//MAME's verified software-list contract cycles 32 2 KiB games on machine reset.
//Stella instead presents the same image as frontend-selected slices; no PCB or schematic
//presently proves the physical counter or latch behavior.
if(reset) game++;
else game = 0;
}
auto serialize(serializer& s) -> void override {
s(game);
}
};
+11 -19
View File
@@ -1,9 +1,8 @@
struct Atari16k : Interface {
using Interface::Interface;
Atari16k(Cartridge& cartridge, bool hasSaraRam = false) : Interface(cartridge), saraRam(hasSaraRam) {}
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
SaraRam saraRam;
n2 bank;
bool hasRam;
auto load() -> void override {
Interface::load(rom, "program.rom");
@@ -15,44 +14,37 @@ struct Atari16k : Interface {
auto unload() -> void override {
}
auto read(n16 address) -> n8 override {
auto read(n16 address, n8 data) -> n8 override {
if(address == 0x1ff6) bank = 0;
if(address == 0x1ff7) bank = 1;
if(address == 0x1ff8) bank = 2;
if(address == 0x1ff9) bank = 3;
if(address.bit(12)) {
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
if(saraRam.readable(address)) return saraRam.read(address, data);
return rom.read((bank * 0x1000) + (address & 0xfff));
}
return 0xff;
return data;
}
auto write(n16 address, n8 data) -> bool override {
auto write(n16 address, n8 data) -> n8 override {
if(address == 0x1ff6) bank = 0;
if(address == 0x1ff7) bank = 1;
if(address == 0x1ff8) bank = 2;
if(address == 0x1ff9) bank = 3;
if(address >= 0x1000 && address <= 0x107f) {
hasRam = true;
ram.write(address & 0x7f, data);
return true;
}
return false;
saraRam.write(address, data);
return data;
}
auto power() -> void override {
auto power(bool reset) -> void override {
bank = 0;
hasRam = 0;
ram.allocate(128);
saraRam.power();
}
auto serialize(serializer& s) -> void override {
s(bank);
s(hasRam);
s(ram);
saraRam.serialize(s);
}
};
+11 -19
View File
@@ -1,9 +1,8 @@
struct Atari32k : Interface {
using Interface::Interface;
Atari32k(Cartridge& cartridge, bool hasSaraRam = false) : Interface(cartridge), saraRam(hasSaraRam) {}
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
SaraRam saraRam;
n3 bank;
bool hasRam;
auto load() -> void override {
Interface::load(rom, "program.rom");
@@ -15,7 +14,7 @@ struct Atari32k : Interface {
auto unload() -> void override {
}
auto read(n16 address) -> n8 override {
auto read(n16 address, n8 data) -> n8 override {
if(address == 0x1ff4) bank = 0;
if(address == 0x1ff5) bank = 1;
if(address == 0x1ff6) bank = 2;
@@ -26,14 +25,14 @@ struct Atari32k : Interface {
if(address == 0x1ffb) bank = 7;
if(address.bit(12)) {
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
if(saraRam.readable(address)) return saraRam.read(address, data);
return rom.read((bank * 0x1000) + (address & 0xfff));
}
return 0xff;
return data;
}
auto write(n16 address, n8 data) -> bool override {
auto write(n16 address, n8 data) -> n8 override {
if(address == 0x1ff4) bank = 0;
if(address == 0x1ff5) bank = 1;
if(address == 0x1ff6) bank = 2;
@@ -43,24 +42,17 @@ struct Atari32k : Interface {
if(address == 0x1ffa) bank = 6;
if(address == 0x1ffb) bank = 7;
if(address >= 0x1000 && address <= 0x107f) {
hasRam = true;
ram.write(address & 0x7f, data);
return true;
}
return false;
saraRam.write(address, data);
return data;
}
auto power() -> void override {
auto power(bool reset) -> void override {
bank = 0;
hasRam = 0;
ram.allocate(128);
saraRam.power();
}
auto serialize(serializer& s) -> void override {
s(bank);
s(hasRam);
s(ram);
saraRam.serialize(s);
}
};
+12 -20
View File
@@ -1,9 +1,8 @@
struct Atari8k : Interface {
using Interface::Interface;
Atari8k(Cartridge& cartridge, bool hasSaraRam = false) : Interface(cartridge), saraRam(hasSaraRam) {}
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
SaraRam saraRam;
n1 bank;
bool hasRam;
auto load() -> void override {
Interface::load(rom, "program.rom");
@@ -15,40 +14,33 @@ struct Atari8k : Interface {
auto unload() -> void override {
}
auto read(n16 address) -> n8 override {
auto read(n16 address, n8 data) -> n8 override {
if(address == 0x1ff8) bank = 0;
if(address == 0x1ff9) bank = 1;
if(address.bit(12)) {
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
if(saraRam.readable(address)) return saraRam.read(address, data);
return rom.read((bank * 0x1000) + (address & 0xfff));
}
return 0xff;
return data;
}
auto write(n16 address, n8 data) -> bool override {
auto write(n16 address, n8 data) -> n8 override {
if(address == 0x1ff8) bank = 0;
if(address == 0x1ff9) bank = 1;
if(address >= 0x1000 && address <= 0x107f) {
hasRam = true;
ram.write(address & 0x7f, data);
return true;
}
return false;
saraRam.write(address, data);
return data;
}
auto power() -> void override {
bank = 0;
hasRam = 0;
ram.allocate(128);
auto power(bool reset) -> void override {
bank = 1;
saraRam.power();
}
auto serialize(serializer& s) -> void override {
s(bank);
s(hasRam);
s(ram);
saraRam.serialize(s);
}
};
+64
View File
@@ -1,12 +1,45 @@
namespace Board {
#include "sara-ram.cpp"
#include "linear.cpp"
#include "activision.cpp"
#include "atari8k.cpp"
#include "atari16k.cpp"
#include "atari32k.cpp"
#include "commavid.cpp"
#include "parker-bros.cpp"
#include "parker-bros-03e0.cpp"
#include "jvp.cpp"
#include "tigervision.cpp"
#include "ua.cpp"
#include "cbs-ram-plus.cpp"
#include "m-network.cpp"
#include "amiga-fc.cpp"
#include "wickstead.cpp"
#include "jane.cpp"
#include "dpc.cpp"
#include "mega-boy.cpp"
#include "atari-32-in-1.cpp"
//Homebrew
#include "dpc-plus.cpp"
#include "cdf.cpp"
#include "bus.cpp"
#include "chetiry.cpp"
#include "fa2.cpp"
#include "elf.cpp"
#include "movie-cart.cpp"
#include "cpuwiz-4ksc.cpp"
#include "three-e.cpp"
#include "three-ex.cpp"
#include "three-e-plus.cpp"
#include "enhanced-3f.cpp"
#include "four-a50.cpp"
#include "ef.cpp"
#include "mdm.cpp"
#include "x07.cpp"
#include "econo-banking.cpp"
#include "superbanking.cpp"
auto Interface::load(Memory::Readable<n8>& memory, string name) -> bool {
if(auto fp = pak->read(name)) {
@@ -34,4 +67,35 @@ auto Interface::save(Memory::Writable<n8>& memory, string name) -> bool {
return false;
}
auto Interface::readARMMemory(Memory::Readable<n8>& rom, Memory::Writable<n8>& ram,
u32 mode, n32 address, u32 romBase, u32 ramLimit, n32& data) -> Harmony::Access {
u32 size = mode & ARM7TDMI::Byte ? 1 : mode & ARM7TDMI::Half ? 2 : 4;
u32 location = address;
auto read = [&](auto& memory, u32 offset) -> Harmony::Access {
if(offset > memory.size() || size > memory.size() - offset) return Harmony::Access::Fault;
data = 0;
for(u32 byte : range(size)) data |= memory.read(offset + byte) << byte * 8;
return Harmony::Access::Granted;
};
if(location >= romBase && location < rom.size()) return read(rom, location);
if(location >= 0x40000000 && location - 0x40000000 < ramLimit) {
return read(ram, location - 0x40000000);
}
return Harmony::Access::Unmapped;
}
auto Interface::writeARMMemory(Memory::Writable<n8>& ram, u32 mode, n32 address,
u32 ramLimit, n32 data) -> Harmony::Access {
u32 size = mode & ARM7TDMI::Byte ? 1 : mode & ARM7TDMI::Half ? 2 : 4;
u32 location = address;
if(location < 0x40000000 || location - 0x40000000 >= ramLimit) return Harmony::Access::Unmapped;
auto offset = location - 0x40000000;
if(offset > ram.size() || size > ram.size() - offset || size > ramLimit - offset) {
return Harmony::Access::Fault;
}
for(u32 byte : range(size)) ram.write(offset + byte, data >> byte * 8);
return Harmony::Access::Granted;
}
}
+18 -3
View File
@@ -8,15 +8,30 @@ struct Interface {
virtual auto load() -> void {}
virtual auto save() -> void {}
virtual auto unload() -> void {}
virtual auto read(n16 address) -> n8 { return 0xff; }
virtual auto write(n16 address, n8 data) -> bool { return false; }
virtual auto power() -> void {}
virtual auto read(n16 address, n8 data) -> n8 { return data; }
virtual auto write(n16 address, n8 data) -> n8 { return data; }
virtual auto power(bool reset) -> void {}
virtual auto serialize(serializer&) -> void {}
virtual auto armInvocation() const -> Harmony::Invocation { return {}; }
virtual auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access {
return Harmony::Access::Unmapped;
}
virtual auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access {
return Harmony::Access::Unmapped;
}
virtual auto trapARM(u32 address, n32& value, n32 argument) -> bool { return false; }
virtual auto stepARM(u32 clocks) -> void {}
auto load(Memory::Readable<n8>&, string name) -> bool;
auto load(Memory::Writable<n8>&, string name) -> bool;
auto save(Memory::Writable<n8>&, string name) -> bool;
auto readARMMemory(Memory::Readable<n8>& rom, Memory::Writable<n8>& ram,
u32 mode, n32 address, u32 romBase, u32 ramLimit, n32& data) -> Harmony::Access;
auto writeARMMemory(Memory::Writable<n8>& ram, u32 mode, n32 address,
u32 ramLimit, n32 data) -> Harmony::Access;
Cartridge& cartridge;
};
+354
View File
@@ -0,0 +1,354 @@
struct BUS : Interface {
using Interface::Interface;
enum class Revision : u32 { BUS0, BUS1, BUS2, BUS3 };
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
Revision revision = Revision::BUS0;
n3 bank;
n8 mode;
n2 fastJumpActive;
n16 styOperand;
n16 busOverdriveAddress;
n16 jumpOperand;
n32 musicCounter[3];
n32 musicFrequency[3];
n8 waveformShift[3];
u16 datastreamBase = 0;
u16 incrementBase = 0;
u16 mapBase = 0;
u16 waveformBase = 0;
u32 audioPhase = 0;
static constexpr u8 CommunicationStream = 0x10;
static constexpr u8 JumpStream = 0x11;
template<typename MemoryType>
auto read32(u32 address, const MemoryType& memory) const -> u32 {
if(address + 3 >= memory.size()) return 0;
return memory.read(address + 0) << 0 | memory.read(address + 1) << 8
| memory.read(address + 2) << 16 | memory.read(address + 3) << 24;
}
auto write32(u32 address, u32 data) -> void {
if(address + 3 >= ram.size()) return;
for(u32 byte : range(4)) ram.write(address + byte, data >> byte * 8);
}
auto scan(u32 value) const -> u32 {
for(u32 offset = 0; offset + 3 < min<u32>(rom.size(), 3_KiB); offset += 4) {
if(read32(offset, rom) == value) return offset;
}
return ~0u;
}
auto detectRevision() -> void {
auto signature = scan(0x00535542);
if(signature == 0x07f4) revision = Revision::BUS1;
else if(signature == 0x0778) revision = Revision::BUS2;
else if(signature == 0x0770) revision = Revision::BUS3;
else revision = Revision::BUS0;
if(revision == Revision::BUS0) {
datastreamBase = 0x0ae0;
incrementBase = 0x0b20;
mapBase = 0x0b64;
waveformBase = 0;
} else {
datastreamBase = revision == Revision::BUS3 ? 0x06d8 : 0x06e0;
incrementBase = 0x0720;
mapBase = 0x0760;
waveformBase = 0x07f4;
}
}
auto load() -> void override {
Interface::load(rom, "program.rom");
detectRevision();
ram.allocate(8_KiB, 0x00);
harmony.load(cartridge.node);
}
auto unload() -> void override {
harmony.unload();
}
auto armInvocation() const -> Harmony::Invocation override {
if(revision == Revision::BUS0) return {.pc = 0x0c08, .lr = 0x0c00, .sp = 0x40001ffc};
return {.pc = 0x0808, .lr = 0x0800, .sp = 0x40001ffc};
}
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access override {
auto romBase = mode & ARM7TDMI::Prefetch ? 0x50u : 0x0750u;
return readARMMemory(rom, ram, mode, address, romBase, min<u32>(8_KiB, ram.size()), data);
}
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access override {
u32 location = address;
if(location >= 0x40000000 && location - 0x40000000 < min<u32>(8_KiB, ram.size())) {
auto offset = location - 0x40000000;
if(offset > 0x28 && offset < 0x06d8) return Harmony::Access::Fault;
}
return writeARMMemory(ram, mode, address, min<u32>(8_KiB, ram.size()), data);
}
auto trapARM(u32 address, n32& voice, n32 value) -> bool override {
constexpr u32 base = 0x06da;
if(address < base || address > base + 12 || (address - base) % 4) return false;
if(voice >= 3) {
if(address == base + 8) voice = 0;
return true;
}
if(address == base + 0) musicFrequency[voice] = value;
if(address == base + 4) musicCounter[voice] = 0;
if(address == base + 8) voice = musicCounter[voice];
if(address == base + 12) waveformShift[voice] = min<u32>(value, 31);
return true;
}
auto stepARM(u32 clocks) -> void override {
stepAudio(clocks);
}
auto programBase() const -> u32 {
return revision == Revision::BUS0 ? 3_KiB : 4_KiB;
}
auto displayBase() const -> u32 {
return revision == Revision::BUS0 ? 3_KiB : 2_KiB;
}
auto program(u32 address) const -> n8 {
auto offset = programBase() + u32{bank} * 4_KiB + (address & 0x0fff);
if(offset < rom.size()) return rom.read(offset);
return 0;
}
auto displayRead(u32 address) const -> u8 {
if(displayBase() + address < ram.size()) return ram.read(displayBase() + address);
return 0;
}
auto displayWrite(u32 address, u8 data) -> void {
if(displayBase() + address < ram.size()) ram.write(displayBase() + address, data);
}
auto streamPointer(u8 index) const -> u32 {
return read32(datastreamBase + index * 4, ram);
}
auto setStreamPointer(u8 index, u32 value) -> void {
write32(datastreamBase + index * 4, value);
}
auto streamIncrement(u8 index) const -> u32 {
return read32(incrementBase + index * 4, ram);
}
auto setStreamIncrement(u8 index, u32 value) -> void {
write32(incrementBase + index * 4, value);
}
auto addressMap(u8 index) const -> u32 {
return read32(mapBase + index * 4, ram);
}
auto setAddressMap(u8 index, u32 value) -> void {
write32(mapBase + index * 4, value);
}
auto nextStreamByte(u8 index) -> u8 {
auto pointer = streamPointer(index);
auto result = displayRead(pointer >> 20);
pointer += streamIncrement(index) << 12;
setStreamPointer(index, pointer);
return result;
}
auto stepAudio(u32 clocks) -> void {
audioPhase += clocks;
auto ticks = audioPhase / 3500;
audioPhase %= 3500;
for(u32 voice : range(3)) musicCounter[voice] += u32{musicFrequency[voice]} * ticks;
}
auto waveform(u32 voice) const -> u32 {
auto address = read32(waveformBase + voice * 4, ram);
if(address < 0x40000800) return 0;
address -= 0x40000800;
return address < 4_KiB ? address : 0;
}
auto amplitude() -> u8 {
if((mode & 0xf0) == 0) {
auto address = read32(waveformBase, ram) + (u32{musicCounter[0]} >> 21);
u8 sample = 0;
if(address < rom.size()) sample = rom.read(address);
else if(address >= 0x40000000 && address - 0x40000000 < ram.size()) {
sample = ram.read(address - 0x40000000);
}
if(!(u32{musicCounter[0]} & 1 << 20)) sample >>= 4;
return sample & 15;
}
u32 result = 0;
for(u32 voice : range(3)) {
result += displayRead(waveform(voice) + (u32{musicCounter[voice]} >> waveformShift[voice]));
}
return result;
}
auto switchBank(u32 address) -> void {
if(revision == Revision::BUS0) {
if(address >= 0x0ff6 && address <= 0x0ffb) bank = address - 0x0ff6;
return;
}
if(address >= 0x0ff5 && address <= 0x0ffb) bank = address - 0x0ff5;
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
u32 offset = address & 0x0fff;
u8 value = program(offset);
if(revision == Revision::BUS3) {
if(fastJumpActive && jumpOperand == offset) {
fastJumpActive--;
jumpOperand++;
auto pointer = streamPointer(JumpStream);
auto result = displayRead(pointer >> 20);
setStreamPointer(JumpStream, pointer + 0x00100000);
return result;
}
if((mode & 15) == 0 && value == 0x4c && offset <= 0x0ffd
&& program(offset + 1) == 0 && program(offset + 2) == 0) {
fastJumpActive = 2;
jumpOperand = offset + 1;
return value;
}
jumpOperand = 0;
}
if((mode & 15) == 0 && styOperand == offset) busOverdriveAddress = value;
styOperand = 0;
if(revision == Revision::BUS0 && offset < 0x10) return nextStreamByte(offset);
if((revision == Revision::BUS1 || revision == Revision::BUS2) && offset < 0x20) {
if(offset < 0x10) return nextStreamByte(offset);
if(offset == 0x18) return amplitude();
return 0;
}
if(revision == Revision::BUS3 && offset >= 0x0fee && offset <= 0x0ff3) {
if(offset == 0x0fee) return amplitude();
if(offset == 0x0fef) return nextStreamByte(CommunicationStream);
}
switchBank(offset);
if((mode & 15) == 0 && value == 0x84) styOperand = offset + 1;
return value;
}
auto writeBelowCartridge(n16 address, n8 data) -> n8 {
bool matches = address == busOverdriveAddress;
busOverdriveAddress = 0xff;
if(!matches) return data;
auto target = u8(address & 0x7f);
if(target > 0x24) return data;
auto mappings = addressMap(target);
auto stream = u8(mappings & 15);
auto stuffed = u8(data) & nextStreamByte(stream);
setAddressMap(target, mappings >> 4 | u32{stream} << 28);
return stuffed;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return writeBelowCartridge(address, data);
u32 offset = address & 0x0fff;
if(revision == Revision::BUS0) {
auto index = u8(offset & 15);
if(offset >= 0x10 && offset <= 0x13) {
auto pointer = streamPointer(index);
displayWrite(pointer >> 20, data);
setStreamPointer(index, pointer + 0x00100000);
}
if(offset == 0x1b && (data == 254 || data == 255)) harmony.call(data == 254);
if(offset == 0x1c) mode = data == 0 ? 0 : 0x0f;
if(offset >= 0x20 && offset <= 0x2f) {
auto pointer = streamPointer(index) << 8;
setStreamPointer(index, pointer & 0xf0000000 | u32{data} << 20);
}
if(offset >= 0x30 && offset <= 0x3f) setStreamIncrement(index, data);
switchBank(offset);
return data;
}
switchBank(offset);
if(revision == Revision::BUS3) {
if(offset == 0x0ff0) {
auto pointer = streamPointer(CommunicationStream);
displayWrite(pointer >> 20, data);
setStreamPointer(CommunicationStream, pointer + 0x00100000);
}
if(offset == 0x0ff1) {
auto pointer = streamPointer(CommunicationStream) << 8;
setStreamPointer(CommunicationStream, pointer & 0xf0000000 | u32{data} << 20);
}
if(offset == 0x0ff2) mode = data;
if(offset == 0x0ff3 && (data == 254 || data == 255)) harmony.call(data == 254);
return data;
}
if(offset >= 0x10 && offset <= 0x1f) {
auto index = u8(offset & 15);
if(index <= 3) {
auto pointer = streamPointer(index);
displayWrite(pointer >> 20, data);
setStreamPointer(index, pointer + 0x00100000);
}
if(index >= 4 && index <= 7) {
index &= 3;
auto pointer = streamPointer(index) << 8;
setStreamPointer(index, pointer & 0xf0000000 | u32{data} << 20);
}
if(index == 9) mode = data == 0 ? 0 : 0x0f;
if(index == 10 && (data == 254 || data == 255)) harmony.call(data == 254);
}
return data;
}
auto power(bool reset) -> void override {
ram.fill(0x00);
auto driverSize = revision == Revision::BUS0 ? 3_KiB : 2_KiB;
for(u32 offset : range(min<u32>(driverSize, rom.size()))) ram.write(offset, rom.read(offset));
bank = revision == Revision::BUS0 ? 5 : 6;
mode = 0xff;
fastJumpActive = 0;
styOperand = 0;
busOverdriveAddress = 0;
jumpOperand = 0;
for(auto& item : musicCounter) item = 0;
for(auto& item : musicFrequency) item = 0;
for(auto& item : waveformShift) item = 27;
audioPhase = 0;
harmony.power();
}
auto serialize(serializer& s) -> void override {
s(ram);
s(bank);
s(mode);
s(fastJumpActive);
s(styOperand);
s(busOverdriveAddress);
s(jumpOperand);
for(auto& item : musicCounter) s(item);
for(auto& item : musicFrequency) s(item);
for(auto& item : waveformShift) s(item);
s(audioPhase);
harmony.serialize(s);
}
};
+40
View File
@@ -0,0 +1,40 @@
struct CbsRamPlus : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n2 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
if(address >= 0x1ff8 && address <= 0x1ffa) bank = address - 0x1ff8;
if(address >= 0x1000 && address <= 0x10ff) {
ram.write(address & 0xff, data);
return data;
}
if(address >= 0x1100 && address <= 0x11ff) return ram.read(address & 0xff);
if(address.bit(12)) return rom.read((bank * 0x1000) + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
if(address >= 0x1ff8 && address <= 0x1ffa) bank = address - 0x1ff8;
if(address >= 0x1000 && address <= 0x10ff) {
ram.write(address & 0xff, data);
return data;
}
return data;
}
auto power(bool reset) -> void override {
bank = 2;
ram.allocate(256, 0x00);
}
auto serialize(serializer& s) -> void override {
s(bank);
s(ram);
}
};
+354
View File
@@ -0,0 +1,354 @@
struct CDF : Interface {
using Interface::Interface;
enum class Revision : u32 { CDF0, CDF1, CDFJ, CDFJPlus };
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
Revision revision = Revision::CDF0;
n3 bank;
n8 mode;
n2 fastJumpActive;
n6 fastJumpStream;
n16 immediateOperand;
n16 jumpOperand;
n32 musicCounter[3];
n32 musicFrequency[3];
n8 waveformShift[3];
u16 datastreamBase = 0;
u16 incrementBase = 0;
u16 waveformBase = 0;
u16 fastFetcherOffset = 0;
u8 amplitudeStream = 0;
u8 fastJumpMask = 0;
bool ldxEnabled = false;
bool ldyEnabled = false;
u32 audioPhase = 0;
static constexpr u8 CommunicationStream = 0x20;
static constexpr u8 JumpStreamBase = 0x21;
static constexpr u16 NoImmediate = 0xffff;
template<typename MemoryType>
auto read32(u32 address, const MemoryType& memory) const -> u32 {
if(address + 3 >= memory.size()) return 0;
return memory.read(address + 0) << 0 | memory.read(address + 1) << 8
| memory.read(address + 2) << 16 | memory.read(address + 3) << 24;
}
auto write32(u32 address, u32 data) -> void {
if(address + 3 >= ram.size()) return;
for(u32 byte : range(4)) ram.write(address + byte, data >> byte * 8);
}
auto scan(u32 value) const -> u32 {
if(rom.size() < 2_KiB) return ~0u;
for(u32 offset = 0; offset < 2_KiB; offset += 4) {
if(read32(offset, rom) == value) return offset;
}
return ~0u;
}
auto detectRevision() -> void {
auto plusSignature = scan(0x53554c50);
if(plusSignature != ~0u && read32(plusSignature + 4, rom) == 0x4a464443
&& read32(plusSignature + 8, rom) == 1) {
revision = Revision::CDFJPlus;
amplitudeStream = 0x23;
fastJumpMask = 0xfe;
datastreamBase = 0x0098;
incrementBase = 0x0124;
waveformBase = 0x01b0;
for(u32 offset = 0; offset < 2_KiB; offset += 4) {
auto word = read32(offset, rom);
if(word == 0x135200a2) ldxEnabled = true;
if(word == 0x135200a0) ldyEnabled = true;
if((word & 0xffffff00) == 0xe2422000) fastFetcherOffset = offset;
}
return;
}
u8 subversion = 0;
for(u32 offset = 0; offset + 11 < min<u32>(rom.size(), 2_KiB); offset += 4) {
if(rom.read(offset + 0) == 'C' && rom.read(offset + 1) == 'D' && rom.read(offset + 2) == 'F'
&& rom.read(offset + 4) == 'C' && rom.read(offset + 5) == 'D' && rom.read(offset + 6) == 'F'
&& rom.read(offset + 8) == 'C' && rom.read(offset + 9) == 'D' && rom.read(offset + 10) == 'F') {
subversion = rom.read(offset + 3);
break;
}
}
if(subversion == 0x4a) {
revision = Revision::CDFJ;
amplitudeStream = 0x23;
fastJumpMask = 0xfe;
datastreamBase = 0x0098;
incrementBase = 0x0124;
waveformBase = 0x01b0;
} else if(subversion == 0) {
revision = Revision::CDF0;
amplitudeStream = 0x22;
fastJumpMask = 0xff;
datastreamBase = 0x06e0;
incrementBase = 0x0768;
waveformBase = 0x07f0;
} else {
revision = Revision::CDF1;
amplitudeStream = 0x22;
fastJumpMask = 0xff;
datastreamBase = 0x00a0;
incrementBase = 0x0128;
waveformBase = 0x01b0;
}
}
auto isPlus() const -> bool {
return revision == Revision::CDFJPlus;
}
auto load() -> void override {
Interface::load(rom, "program.rom");
detectRevision();
ram.allocate(isPlus() ? 32_KiB : 8_KiB, 0x00);
harmony.load(cartridge.node);
}
auto unload() -> void override {
harmony.unload();
}
auto armInvocation() const -> Harmony::Invocation override {
if(isPlus()) {
auto address = read32(0x17f8, rom) & ~1u;
return {.pc = address, .lr = address, .sp = read32(0x17f4, rom)};
}
return {.pc = 0x0808, .lr = 0x0800, .sp = 0x40001ffc};
}
auto armRamLimit() const -> u32 {
if(!isPlus()) return min<u32>(8_KiB, ram.size());
if(rom.size() == 64_KiB || rom.size() == 128_KiB) return min<u32>(16_KiB, ram.size());
if(rom.size() == 256_KiB || rom.size() == 512_KiB) return min<u32>(32_KiB, ram.size());
return min<u32>(8_KiB, ram.size());
}
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access override {
auto romBase = mode & ARM7TDMI::Prefetch ? 0x50u : 0x0750u;
return readARMMemory(rom, ram, mode, address, romBase, armRamLimit(), data);
}
auto writeProtectedARM(u32 offset) const -> bool {
if(revision == Revision::CDF0) {
return offset > 0x28 && offset < 0x0800 && !(offset >= 0x06e0 && offset < 0x07fc);
}
if(revision == Revision::CDF1) {
return offset > 0x28 && offset < 0x0800 && !(offset >= 0x00a0 && offset < 0x01bc);
}
return offset > 0x28 && offset < 0x0800 && !(offset >= 0x0098 && offset < 0x01bc)
&& !(isPlus() && offset == 0x03e0);
}
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access override {
u32 location = address;
if(location >= 0x40000000 && location - 0x40000000 < armRamLimit()) {
if(writeProtectedARM(location - 0x40000000)) return Harmony::Access::Fault;
}
return writeARMMemory(ram, mode, address, armRamLimit(), data);
}
auto trapARM(u32 address, n32& voice, n32 value) -> bool override {
u32 base = revision == Revision::CDF0 ? 0x06e2 : 0x0752;
if(address < base || address > base + 12 || (address - base) % 4) return false;
if(voice >= 3) {
if(address == base + 8) voice = 0;
return true;
}
if(address == base + 0) musicFrequency[voice] = value;
if(address == base + 4) musicCounter[voice] = 0;
if(address == base + 8) voice = musicCounter[voice];
if(address == base + 12) waveformShift[voice] = min<u32>(value, 31);
return true;
}
auto stepARM(u32 clocks) -> void override {
stepAudio(clocks);
}
auto programBase() const -> u32 {
return isPlus() ? 2_KiB : 4_KiB;
}
auto program(u32 address) const -> n8 {
u32 offset = programBase() + u32{bank} * 4_KiB + (address & 0x0fff);
if(offset < rom.size()) return rom.read(offset);
return 0;
}
auto displaySize() const -> u32 {
return ram.size() > 2_KiB ? ram.size() - 2_KiB : 0;
}
auto displayRead(u32 address) const -> u8 {
if(address < displaySize()) return ram.read(2_KiB + address);
return 0;
}
auto displayWrite(u32 address, u8 data) -> void {
if(address < displaySize()) ram.write(2_KiB + address, data);
}
auto streamPointer(u8 index) const -> u32 {
return read32(datastreamBase + index * 4, ram);
}
auto setStreamPointer(u8 index, u32 value) -> void {
write32(datastreamBase + index * 4, value);
}
auto streamIncrement(u8 index) const -> u32 {
return read32(incrementBase + index * 4, ram);
}
auto nextStreamByte(u8 index) -> u8 {
if(index > amplitudeStream) return 0;
auto pointer = streamPointer(index);
u32 location = isPlus() ? pointer >> 16 : pointer >> 20;
auto data = displayRead(location);
pointer += streamIncrement(index) << (isPlus() ? 8 : 12);
setStreamPointer(index, pointer);
return data;
}
auto waveform(u32 voice) const -> u32 {
u32 address = read32(waveformBase + voice * 4, ram);
address -= 0x40000800;
if(!isPlus() && address >= 4_KiB) address &= 0x0fff;
if(isPlus() && address >= displaySize()) return 0;
return address;
}
auto stepAudio(u32 clocks) -> void {
audioPhase += clocks;
auto ticks = audioPhase / 3500;
audioPhase %= 3500;
for(u32 voice : range(3)) musicCounter[voice] += u32{musicFrequency[voice]} * ticks;
}
auto audioValue() -> u8 {
if((mode & 0xf0) == 0) {
u32 address = read32(waveformBase, ram) + (u32{musicCounter[0]} >> (isPlus() ? 13 : 21));
u8 sample = 0;
if(address < rom.size()) sample = rom.read(address);
else if(address >= 0x40000000 && address - 0x40000000 < ram.size()) sample = ram.read(address - 0x40000000);
if(!(u32{musicCounter[0]} & 1 << (isPlus() ? 12 : 20))) sample >>= 4;
return sample & 15;
}
u32 result = 0;
for(u32 voice : range(3)) {
auto location = waveform(voice) + (u32{musicCounter[voice]} >> waveformShift[voice]);
result += displayRead(location);
}
return result;
}
auto switchBank(u32 address) -> void {
if(address < 0x0ff4 || address > 0x0ffb) return;
if(isPlus()) bank = address == 0x0ffb ? 0 : address - 0x0ff4;
else bank = address == 0x0ff4 || address == 0x0ffb ? 6 : address - 0x0ff5;
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
u32 offset = address & 0x0fff;
u8 value = program(offset);
if(fastJumpActive && jumpOperand == offset) {
fastJumpActive--;
jumpOperand++;
auto pointer = streamPointer(fastJumpStream);
auto result = displayRead(isPlus() ? pointer >> 16 : pointer >> 20);
pointer += isPlus() ? 0x00010000 : 0x00100000;
setStreamPointer(fastJumpStream, pointer);
return result;
}
if((mode & 15) == 0 && value == 0x4c && offset <= 0x0ffd
&& !(program(offset + 1) & fastJumpMask) && program(offset + 2) == 0) {
fastJumpActive = 2;
jumpOperand = offset + 1;
fastJumpStream = program(offset + 1) + JumpStreamBase;
return value;
}
jumpOperand = 0;
bool fastFetch = false;
if((mode & 15) == 0 && immediateOperand == offset) {
u8 base = 0;
if(fastFetcherOffset && fastFetcherOffset < ram.size()) base = ram.read(fastFetcherOffset);
fastFetch = value >= base && u32{value} <= u32{base} + amplitudeStream;
if(fastFetch && fastFetcherOffset) value -= base;
}
immediateOperand = NoImmediate;
if(fastFetch) return value == amplitudeStream ? audioValue() : nextStreamByte(value);
switchBank(offset);
if((mode & 15) == 0 && (value == 0xa9 || (ldxEnabled && value == 0xa2) || (ldyEnabled && value == 0xa0))) {
immediateOperand = offset + 1;
}
return value;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
u32 offset = address & 0x0fff;
if(offset == 0x0ff0) {
auto pointer = streamPointer(CommunicationStream);
displayWrite(isPlus() ? pointer >> 16 : pointer >> 20, data);
setStreamPointer(CommunicationStream, pointer + (isPlus() ? 0x00010000 : 0x00100000));
}
if(offset == 0x0ff1) {
auto pointer = streamPointer(CommunicationStream) << 8;
pointer = isPlus() ? (pointer & 0xff000000) | u32{data} << 16
: (pointer & 0xf0000000) | u32{data} << 20;
setStreamPointer(CommunicationStream, pointer);
}
if(offset == 0x0ff2) mode = data;
if(offset == 0x0ff3 && (data == 254 || data == 255)) harmony.call(data == 254);
switchBank(offset);
return data;
}
auto power(bool reset) -> void override {
ram.fill(0x00);
for(u32 offset : range(min<u32>(2_KiB, rom.size()))) ram.write(offset, rom.read(offset));
bank = isPlus() ? 0 : 6;
mode = 0xff;
fastJumpActive = 0;
fastJumpStream = 0;
immediateOperand = NoImmediate;
jumpOperand = 0;
for(auto& item : musicCounter) item = 0;
for(auto& item : musicFrequency) item = 0;
for(auto& item : waveformShift) item = 27;
audioPhase = 0;
harmony.power();
}
auto serialize(serializer& s) -> void override {
s(ram);
s(bank);
s(mode);
s(fastJumpActive);
s(fastJumpStream);
s(immediateOperand);
s(jumpOperand);
for(auto& item : musicCounter) s(item);
for(auto& item : musicFrequency) s(item);
for(auto& item : waveformShift) s(item);
s(audioPhase);
harmony.serialize(s);
}
};
+212
View File
@@ -0,0 +1,212 @@
struct Chetiry : Interface, Thread {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Readable<n8> tunes;
Memory::Writable<n8> ram;
PersistentMemory persistent;
n3 bank;
n8 operation;
n16 tunePosition;
n1 ldaImmediate;
n32 random = 0x2b435044;
n32 musicCounter[3];
n32 musicFrequency[3];
n15 busyTicks;
n1 operationCompleted;
n1 operationSucceeded;
n3 tune;
auto load() -> void override {
Memory::Readable<n8> image;
Interface::load(image, "program.rom");
rom.allocate(32_KiB, 0x00);
for(u32 offset : range(min<u32>(image.size(), 32_KiB))) rom.program(offset, image.read(offset));
tunes.allocate(28_KiB, 0x00);
if(image.size() > 32_KiB) {
for(u32 offset : range(min<u32>(image.size() - 32_KiB, 28_KiB))) {
tunes.program(offset, image.read(32_KiB + offset));
}
}
persistent.load(pak, "save.eeprom", 256, 0x00);
}
auto save() -> void override {
persistent.flush(pak);
}
auto unload() -> void override {
persistent.flush(pak);
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
auto value = rom.read(bank * 4_KiB + (address & 0x0fff));
if(ldaImmediate && value == 0xf2) {
ldaImmediate = 0;
return (musicCounter[0] >> 31) + (musicCounter[1] >> 31) + (musicCounter[2] >> 31) << 2;
}
ldaImmediate = 0;
if(address <= 0x103f) {
ram.write(address & 0x3f, data);
return data;
}
if(address <= 0x107f) return readRAM(address & 0x3f);
if(address == 0x1ff4) return operate(value);
bankswitch(address);
ldaImmediate = value == 0xa9;
return value;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
if(address <= 0x103f) {
writeRAM(address & 0x3f, data);
return data;
}
if(address == 0x1ff4) {
operate(rom.read(bank * 4_KiB + 0x0ff4));
return data;
}
bankswitch(address);
return data;
}
auto main() -> void {
for(u32 channel : range(3)) musicCounter[channel] += musicFrequency[channel];
if(busyTicks && !--busyTicks) {
operationCompleted = 1;
ram.write(0, operationSucceeded ? 0x00 : 0xff);
}
step(1);
Thread::synchronize(cpu);
}
auto power(bool reset) -> void override {
Thread::create(20'000, std::bind_front(&Chetiry::main, this));
bank = 1;
operation = 0;
tunePosition = 0;
ldaImmediate = 0;
random = 0x2b435044;
busyTicks = 0;
operationCompleted = 0;
operationSucceeded = 0;
tune = 0;
for(auto& counter : musicCounter) counter = 0;
for(auto& frequency : musicFrequency) frequency = 0;
ram.allocate(64, 0x00);
for(u32 offset : range(4)) ram.write(offset, 0xff);
}
auto serialize(serializer& s) -> void override {
Thread::serialize(s);
s(bank);
s(operation);
s(tunePosition);
s(ldaImmediate);
s(random);
s(musicCounter);
s(musicFrequency);
s(busyTicks);
s(operationCompleted);
s(operationSucceeded);
s(tune);
s(ram);
}
private:
static auto noteFrequency(u32 note) -> u32 {
static constexpr u32 table[63] = {
0, 0, 0, 11811160, 12513387, 13257490, 14045832, 14881203, 15765966,
16703557, 17696768, 18749035, 19864009, 21045125, 22296464, 23622320,
25026989, 26515195, 28091878, 29762191, 31531932, 33406900, 35393537,
37498071, 39727803, 42090250, 44592927, 47244640, 50053978, 53030391,
56183756, 59524596, 63064079, 66814014, 70787074, 74996142, 79455606,
84180285, 89186069, 94489281, 100107957, 106060567, 112367297,
119048977, 126128157, 133628029, 141573933, 149992288, 158911428,
168360785, 178371925, 188978561, 200215913, 212121348, 224734593,
238098169, 252256099, 267256058, 283147866, 299984783, 317822855,
336721571, 356744064,
};
return note < 63 ? table[note] : 0;
}
auto bankswitch(n16 address) -> void {
if(address >= 0x1ff5 && address <= 0x1ffb) bank = address - 0x1ff4;
}
auto readRAM(u32 address) -> n8 {
if(address == 0) return ram.read(0);
if(address == 1) {
random = (random.bit(10) ? 0x10adab1e : 0) ^ (u32{random} >> 11 | u32{random} << 21);
return random.byte(0);
}
if(address == 2) return tunePosition.byte(0);
if(address == 3) return tunePosition.byte(1);
return ram.read(address);
}
auto writeRAM(u32 address, n8 data) -> void {
if(address == 0) operation = data;
else if(address == 1) random = 0x2b435044;
else if(address == 2) {
tunePosition = 0;
for(auto& counter : musicCounter) counter = 0;
for(auto& frequency : musicFrequency) frequency = 0;
} else if(address == 3) {
updateTune();
} else {
ram.write(address, data);
}
}
auto updateTune() -> void {
auto position = u32{tune} * 4_KiB + u32{tunePosition} * 3;
if(position + 2 >= tunes.size() || position + 2 >= (u32{tune} + 1) * 4_KiB) {
tunePosition = 0;
return;
}
tunePosition++;
auto note0 = tunes.read(position + 0);
auto note1 = tunes.read(position + 1);
auto note2 = tunes.read(position + 2);
if(note0) musicFrequency[0] = noteFrequency(note0);
if(note1) musicFrequency[1] = noteFrequency(note1);
if(note2 == 1) tunePosition = 0;
else musicFrequency[2] = noteFrequency(note2);
}
auto operate(n8 value) -> n8 {
if(busyTicks) return value | 0x40;
if(operationCompleted) {
operationCompleted = 0;
return value & ~0x40;
}
auto index = u32{operation} >> 4;
auto command = u32{operation} & 15;
operationSucceeded = 1;
if(command == 1 && index < 7) {
tune = index;
tunePosition = 0;
busyTicks = 10'000;
} else if(command == 2 && index < 4) {
for(u32 offset : range(4, 64)) ram.write(offset, persistent.read(index * 64 + offset));
busyTicks = 10'000;
} else if(command == 3 && index < 4) {
for(u32 offset : range(4, 64)) persistent.write(index * 64 + offset, ram.read(offset));
busyTicks = 20'000;
} else if(command == 4) {
persistent.erase();
busyTicks = 20'000;
} else {
operationSucceeded = 0;
ram.write(0, 0xff);
return value & ~0x40;
}
return value | 0x40;
}
};
+6 -6
View File
@@ -13,25 +13,25 @@ struct Commavid : Interface {
auto unload() -> void override {
}
auto read(n16 address) -> n8 override {
auto read(n16 address, n8 data) -> n8 override {
if(address.bit(12)) {
if(address >= 0x1000 && address <= 0x13ff) return ram.read(address & 0x3ff);
return rom.read(address & 0xfff);
}
return 0xff;
return data;
}
auto write(n16 address, n8 data) -> bool override {
auto write(n16 address, n8 data) -> n8 override {
if(address >= 0x1400 && address <= 0x17ff) {
ram.write(address & 0x3ff, data);
return true;
return data;
}
return false;
return data;
}
auto power() -> void override {
auto power(bool reset) -> void override {
ram.allocate(1_KiB);
}
+41
View File
@@ -0,0 +1,41 @@
struct CPUWiz4KSC : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto save() -> void override {
}
auto unload() -> void override {
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address < 0x1000) return data;
if(address <= 0x107f) {
ram.write(address & 0x7f, data);
return data;
}
if(address <= 0x10ff) return ram.read(address & 0x7f);
return rom.read(address & 0xfff);
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address < 0x1000 || address > 0x107f) return data;
ram.write(address & 0x7f, data);
return data;
}
auto power(bool reset) -> void override {
ram.allocate(128, 0x00);
}
auto serialize(serializer& s) -> void override {
s(ram);
}
};
+276
View File
@@ -0,0 +1,276 @@
struct DPCPlus : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n3 bank;
n8 top[8];
n8 bottom[8];
n12 counter[8];
n20 fractionalCounter[8];
n8 fractionalIncrement[8];
n32 musicCounter[3];
n32 musicFrequency[3];
n7 musicWaveform[3];
n32 random;
n1 fastFetch;
n1 ldaImmediate;
n8 parameter[8];
n4 parameterPointer;
u32 fractionalLowMask = 0x0f00ff;
u32 audioPhase = 0;
auto load() -> void override {
Memory::Readable<n8> image;
Interface::load(image, "program.rom");
auto size = min<u32>(image.size(), 32_KiB);
rom.allocate(32_KiB, 0x00);
auto destination = 32_KiB - size;
for(u32 offset : range(size)) rom.program(destination + offset, image.read(offset));
if(image.size() >= 3_KiB) {
Hash::SHA256 hash;
for(u32 offset : range(3_KiB)) hash.input((u8)image.read(offset));
auto driver = hash.digest();
if(driver == "bcbe33dc02ad5104f3a3a53a856a23c57f435344790843f4fb6e746193468d0c"
|| driver == "445ca7866a0efdf0e35076a25e963708dc630f80fd82dcdaf775fb80c8b0c58d") {
fractionalLowMask = 0x0f0000;
}
}
ram.allocate(8_KiB, 0x00);
harmony.load(cartridge.node);
}
auto unload() -> void override {
harmony.unload();
}
auto armInvocation() const -> Harmony::Invocation override {
return {.pc = 0x0c08, .lr = 0x0c00, .sp = 0x40001ffc};
}
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access override {
auto romBase = mode & ARM7TDMI::Prefetch ? 0x50u : 0x0c00u;
return readARMMemory(rom, ram, mode, address, romBase, min<u32>(8_KiB, ram.size()), data);
}
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access override {
u32 location = address;
if(location >= 0x40000000 && location - 0x40000000 < min<u32>(8_KiB, ram.size())) {
auto offset = location - 0x40000000;
if(offset > 0x28 && offset < 0x0c00) return Harmony::Access::Fault;
}
return writeARMMemory(ram, mode, address, min<u32>(8_KiB, ram.size()), data);
}
auto stepARM(u32 clocks) -> void override {
stepAudio(clocks);
}
auto program(u32 address) const -> n8 {
return rom.read(3_KiB + bank * 4_KiB + (address & 0x0fff));
}
auto displayRead(u32 address) const -> n8 {
return ram.read(3_KiB + (address & 0x0fff));
}
auto displayWrite(u32 address, n8 data) -> void {
ram.write(3_KiB + (address & 0x0fff), data);
}
auto frequency(u32 note) const -> u32 {
u32 address = 7_KiB + (note << 2);
return ram.read(address + 0) << 0 | ram.read(address + 1) << 8
| ram.read(address + 2) << 16 | ram.read(address + 3) << 24;
}
auto clockRandom() -> void {
random = (random.bit(10) ? 0x10adab1e : 0) ^ (u32{random} >> 11 | u32{random} << 21);
}
auto priorRandom() -> void {
u32 value = random;
if(value >> 31) value ^= 0x10adab1e;
random = value << 11 | value >> 21;
}
auto stepAudio(u32 clocks) -> void {
audioPhase += clocks;
auto ticks = audioPhase / 3500;
audioPhase %= 3500;
for(u32 channel : range(3)) musicCounter[channel] += u32{musicFrequency[channel]} * ticks;
}
auto amplitude() -> n8 {
u32 value = 0;
for(u32 channel : range(3)) {
value += displayRead((u32{musicWaveform[channel]} << 5) + (u32{musicCounter[channel]} >> 27));
}
return value;
}
auto readRegister(u32 address) -> n8 {
u32 index = address & 7;
u32 function = address >> 3 & 7;
u8 topDistance = u8(u32{top[index]} - (u32{counter[index]} & 0xff));
u8 window = u8(u32{top[index]} - u32{bottom[index]});
n8 flag = topDistance > window ? 0xff : 0x00;
if(function == 0) {
if(index == 0) return clockRandom(), random.byte(0);
if(index == 1) return priorRandom(), random.byte(0);
if(index == 2) return random.byte(1);
if(index == 3) return random.byte(2);
if(index == 4) return random.byte(3);
if(index == 5) return amplitude();
return 0;
}
if(function == 1) {
auto data = displayRead(counter[index]);
counter[index]++;
return data;
}
if(function == 2) {
auto data = displayRead(counter[index]) & flag;
counter[index]++;
return data;
}
if(function == 3) {
auto data = displayRead(u32{fractionalCounter[index]} >> 8);
fractionalCounter[index] += fractionalIncrement[index];
return data;
}
if(function == 4 && index < 4) return flag;
return 0;
}
auto callFunction(n8 function) -> void {
u32 source = parameter[0] | parameter[1] << 8;
if(function == 0) parameterPointer = 0;
if(function == 1 || function == 2) {
u32 destination = counter[parameter[2] & 7];
u32 count = parameter[3];
if(destination < 4_KiB) {
count = min(count, 4_KiB - destination);
if(function == 1 && source < rom.size() - 3_KiB) {
count = min(count, rom.size() - 3_KiB - source);
for(u32 offset : range(count)) displayWrite(destination + offset, rom.read(3_KiB + source + offset));
}
if(function == 2) {
for(u32 offset : range(count)) displayWrite(destination + offset, parameter[0]);
}
}
parameterPointer = 0;
}
if(function == 254 || function == 255) {
harmony.call(function == 254);
}
}
auto writeRegister(u32 address, n8 data) -> void {
u32 index = address & 7;
u32 function = (address - 0x28) >> 3 & 15;
if(function == 0) fractionalCounter[index] =
(u32{fractionalCounter[index]} & fractionalLowMask) | u32{data} << 8;
if(function == 1) fractionalCounter[index] =
((u32{data} & 15) << 16) | (u32{fractionalCounter[index]} & 0xffff);
if(function == 2) {
fractionalIncrement[index] = data;
fractionalCounter[index] = u32{fractionalCounter[index]} & 0x0fff00;
}
if(function == 3) top[index] = data;
if(function == 4) bottom[index] = data;
if(function == 5) counter[index] = (u32{counter[index]} & 0x0f00) | data;
if(function == 6) {
if(index == 0) fastFetch = data == 0;
if(index == 1 && parameterPointer < 8) parameter[parameterPointer++] = data;
if(index == 2) callFunction(data);
if(index >= 5) musicWaveform[index - 5] = data & 0x7f;
}
if(function == 7) {
counter[index]--;
displayWrite(counter[index], data);
}
if(function == 8) counter[index] = ((u32{data} & 15) << 8) | (u32{counter[index]} & 0xff);
if(function == 9) {
if(index == 0) random = 0x2b435044;
if(index >= 1 && index <= 4) random.byte(index - 1) = data;
if(index >= 5) musicFrequency[index - 5] = frequency(data);
}
if(function == 10) {
displayWrite(counter[index], data);
counter[index]++;
}
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
u32 offset = address & 0x0fff;
n8 result = program(offset);
if(fastFetch && ldaImmediate && result < 0x28) offset = result;
ldaImmediate = 0;
if(offset < 0x28) return readRegister(offset);
if(offset >= 0x0ff6 && offset <= 0x0ffb) bank = offset - 0x0ff6;
if(fastFetch) ldaImmediate = result == 0xa9;
return result;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
u32 offset = address & 0x0fff;
if(offset >= 0x28 && offset < 0x80) {
writeRegister(offset, data);
return data;
}
if(offset >= 0x0ff6 && offset <= 0x0ffb) bank = offset - 0x0ff6;
return data;
}
auto power(bool reset) -> void override {
ram.fill(0x00);
if(rom.size() >= 32_KiB) {
for(u32 offset : range(5_KiB)) ram.write(3_KiB + offset, rom.read(27_KiB + offset));
}
bank = 5;
for(auto& item : top) item = 0;
for(auto& item : bottom) item = 0;
for(auto& item : counter) item = 0;
for(auto& item : fractionalCounter) item = 0;
for(auto& item : fractionalIncrement) item = 0;
for(auto& item : musicCounter) item = 0;
for(auto& item : musicFrequency) item = 0;
for(auto& item : musicWaveform) item = 0;
for(auto& item : parameter) item = 0;
random = 0x2b435044;
fastFetch = 0;
ldaImmediate = 0;
parameterPointer = 0;
audioPhase = 0;
harmony.power();
}
auto serialize(serializer& s) -> void override {
s(ram);
s(bank);
for(auto& item : top) s(item);
for(auto& item : bottom) s(item);
for(auto& item : counter) s(item);
for(auto& item : fractionalCounter) s(item);
for(auto& item : fractionalIncrement) s(item);
for(auto& item : musicCounter) s(item);
for(auto& item : musicFrequency) s(item);
for(auto& item : musicWaveform) s(item);
s(random);
s(fastFetch);
s(ldaImmediate);
for(auto& item : parameter) s(item);
s(parameterPointer);
s(audioPhase);
harmony.serialize(s);
}
};
+190
View File
@@ -0,0 +1,190 @@
struct DPC : Interface, Thread {
using Interface::Interface;
struct Fetcher {
n8 top;
n8 bottom;
n8 low;
n3 high;
n1 flag;
n1 musicMode;
n1 oscillatorClock;
} fetcher[8];
Memory::Readable<n8> rom;
n1 bank;
n8 random;
n4 movement;
n8 drawLatch;
n8 drawOperand;
n1 drawCarry;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto clockRandom() -> void {
n1 input = !((random.bit(7) ^ random.bit(5)) ^ (random.bit(4) ^ random.bit(3)));
random = random << 1 | input;
}
auto checkFlag(u32 index) -> void {
if(fetcher[index].low == fetcher[index].top) fetcher[index].flag = 1;
if(fetcher[index].low == fetcher[index].bottom) fetcher[index].flag = 0;
}
auto decrement(u32 index) -> void {
if(--fetcher[index].low == 0xff) {
fetcher[index].high--;
if(index >= 5 && fetcher[index].musicMode) fetcher[index].low = fetcher[index].top;
}
checkFlag(index);
}
auto display(u32 index) -> n8 {
auto counter = fetcher[index].high << 8 | fetcher[index].low;
return rom.read(8_KiB + (0x7ff - counter));
}
auto music() -> n4 {
static constexpr u8 amplitude[] = {0x0, 0x4, 0x5, 0x9, 0x6, 0xa, 0xb, 0xf};
u32 state = 0;
if(fetcher[5].musicMode && fetcher[5].flag) state |= 1;
if(fetcher[6].musicMode && fetcher[6].flag) state |= 2;
if(fetcher[7].musicMode && fetcher[7].flag) state |= 4;
return amplitude[state];
}
auto reverse(n8 data) -> n8 {
n8 result;
for(u32 bit : range(8)) result.bit(7 - bit) = data.bit(bit);
return result;
}
auto readRegister(n16 address) -> n8 {
auto index = address & 7;
auto function = address.bit(3, 5);
if(function == 0) {
if(index < 4) return random;
if(index <= 5) {
drawOperand = drawLatch;
auto sum = u32{drawOperand} + u32{fetcher[4].top};
drawLatch = sum;
drawCarry = sum > 0xff;
}
return n8{drawCarry ? movement << 4 : 0} | music();
}
checkFlag(index);
auto data = display(index);
n8 result;
if(function == 1) result = data;
if(function == 2) result = fetcher[index].flag ? data : n8{0};
if(function == 3) result = fetcher[index].flag ? n8{data << 4 | data >> 4} : n8{0};
if(function == 4) result = fetcher[index].flag ? reverse(data) : n8{0};
if(function == 5) result = fetcher[index].flag ? n8{data >> 1} : n8{0};
if(function == 6) result = fetcher[index].flag ? n8{data << 1} : n8{0};
if(function == 7) result = fetcher[index].flag ? 0xff : 0x00;
if(index < 5 || !fetcher[index].oscillatorClock) decrement(index);
return result;
}
auto writeRegister(n16 address, n8 data) -> void {
auto index = address & 7;
auto function = address.bit(3, 5);
if(function == 0) {
fetcher[index].top = data;
fetcher[index].flag = 0;
}
if(function == 1) {
fetcher[index].bottom = data;
checkFlag(index);
}
if(function == 2) {
fetcher[index].low = index >= 5 && fetcher[index].musicMode ? fetcher[index].top : data;
if(index == 4) drawLatch = data;
checkFlag(index);
}
if(function == 3) {
fetcher[index].high = data.bit(0, 2);
if(index >= 5) {
fetcher[index].musicMode = data.bit(4);
fetcher[index].oscillatorClock = data.bit(5);
if(fetcher[index].musicMode && fetcher[index].low == 0xff) {
fetcher[index].low = fetcher[index].top;
checkFlag(index);
}
}
}
if(function == 4) movement = data.bit(4, 7);
if(function == 6) random = 0;
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
clockRandom();
if(address == 0x1ff8) bank = 0;
if(address == 0x1ff9) bank = 1;
if(address <= 0x103f) return readRegister(address);
return rom.read(bank * 4_KiB + (address & 0xfff));
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
clockRandom();
if(address == 0x1ff8) bank = 0;
if(address == 0x1ff9) bank = 1;
if(address >= 0x1040 && address <= 0x107f) {
writeRegister(address, data);
return data;
}
return data;
}
auto main() -> void {
for(u32 index = 5; index < 8; index++) {
if(fetcher[index].oscillatorClock) decrement(index);
}
step(1);
Thread::synchronize(cpu);
}
auto power(bool reset) -> void override {
Thread::create(20'000, std::bind_front(&DPC::main, this));
//Stella latch bank 1.
//MAME latch bank 0.
bank = 1;
random = 0;
movement = 0;
drawLatch = 0;
drawOperand = 0;
drawCarry = 0;
for(auto& item : fetcher) item = {};
}
auto serialize(serializer& s) -> void override {
Thread::serialize(s);
s(bank);
s(random);
s(movement);
s(drawLatch);
s(drawOperand);
s(drawCarry);
for(auto& item : fetcher) {
s(item.top);
s(item.bottom);
s(item.low);
s(item.high);
s(item.flag);
s(item.musicMode);
s(item.oscillatorClock);
}
}
};
@@ -0,0 +1,36 @@
struct EconoBanking : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n1 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
return data;
}
auto power(bool reset) -> void override {
bank = 0;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
private:
auto select(n16 address) -> void {
if((address & 0x1840) == 0x0800) bank = 0;
if((address & 0x1840) == 0x0840) bank = 1;
}
};
+156
View File
@@ -0,0 +1,156 @@
struct EFBase : Interface {
EFBase(Cartridge& cartridge, n16 hotspotMask, n16 hotspot, n6 startupBank, bool hasRam = false)
: Interface(cartridge), hotspotMask(hotspotMask), hotspot(hotspot), startupBank(startupBank), hasRam(hasRam) {}
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
const n16 hotspotMask;
const n16 hotspot;
const n6 startupBank;
const bool hasRam;
n6 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
bankswitch(address);
if(!address.bit(12)) return data;
if(hasRam && address <= 0x107f) {
ram.write(address & 0x7f, data);
return data;
}
if(hasRam && address <= 0x10ff) return ram.read(address & 0x7f);
return rom.read(bank * 0x1000 + (address & 0x0fff));
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
bankswitch(address);
if(hasRam && address >= 0x1000 && address <= 0x107f) {
ram.write(address & 0x7f, data);
return data;
}
return data;
}
auto power(bool reset) -> void override {
bank = startupBank;
if(hasRam) ram.allocate(128, 0x00);
}
auto serialize(serializer& s) -> void override {
s(bank);
if(hasRam) s(ram);
}
private:
auto bankswitch(n16 address) -> void {
if((address & hotspotMask) == hotspot) bank = address - hotspot;
}
};
struct EF : EFBase {
EF(Cartridge& cartridge) : EFBase(cartridge, 0x1ff0, 0x1fe0, 1) {}
};
struct EFF : EFBase {
EFF(Cartridge& cartridge) : EFBase(cartridge, 0x1ff0, 0x1fe0, 1) {}
M24C eeprom;
PersistentMemory persistent;
auto load() -> void override {
EFBase::load();
persistent.load(pak, "save.eeprom", 2_KiB, 0xff);
eeprom.load(M24C::Type::M24C16);
std::copy(persistent.memory.begin(), persistent.memory.end(), std::begin(eeprom.memory));
}
auto save() -> void override {
persistent.flush(pak);
}
auto unload() -> void override {
persistent.flush(pak);
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address >= 0x1ff0 && address <= 0x1ff4) {
if(address == 0x1ff0) setClock(0);
if(address == 0x1ff1) setClock(1);
if(address == 0x1ff2) setData(0);
if(address == 0x1ff3) setData(1);
if(address == 0x1ff4) return rom.read(bank * 0x1000 + 0x0ff4 + eeprom.read());
}
return EFBase::read(address, data);
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address >= 0x1ff0 && address <= 0x1ff3) {
if(address == 0x1ff0) setClock(0);
if(address == 0x1ff1) setClock(1);
if(address == 0x1ff2) setData(0);
if(address == 0x1ff3) setData(1);
}
return EFBase::write(address, data);
}
auto power(bool reset) -> void override {
EFBase::power(reset);
eeprom.power();
}
auto serialize(serializer& s) -> void override {
EFBase::serialize(s);
eeprom.serialize(s, false);
if(s.reading()) {
std::copy(persistent.memory.begin(), persistent.memory.end(), std::begin(eeprom.memory));
}
}
private:
auto synchronizePersistent() -> void {
persistent.replace({(const u8*)eeprom.memory, eeprom.size()});
}
auto setClock(bool value) -> void {
eeprom.clock = eeprom.clock();
eeprom.data = eeprom.data();
eeprom.clock = value;
eeprom.write();
synchronizePersistent();
}
auto setData(bool value) -> void {
eeprom.clock = eeprom.clock();
eeprom.data = eeprom.data();
eeprom.data = value;
eeprom.write();
synchronizePersistent();
}
};
struct DF : EFBase {
DF(Cartridge& cartridge) : EFBase(cartridge, 0x1fe0, 0x1fc0, 15) {}
};
struct BF : EFBase {
BF(Cartridge& cartridge) : EFBase(cartridge, 0x1fc0, 0x1f80, 1) {}
};
struct EFSC : EFBase {
EFSC(Cartridge& cartridge) : EFBase(cartridge, 0x1ff0, 0x1fe0, 15, true) {}
};
struct DFSC : EFBase {
DFSC(Cartridge& cartridge) : EFBase(cartridge, 0x1fe0, 0x1fc0, 15, true) {}
};
struct BFSC : EFBase {
BFSC(Cartridge& cartridge) : EFBase(cartridge, 0x1fc0, 0x1f80, 15, true) {}
};
+89
View File
@@ -0,0 +1,89 @@
struct ELFCartridge : Interface {
ELFCartridge(Cartridge& cartridge) : Interface(cartridge) {}
auto fail(string reason) -> void {
runtime.unload();
publishedError = std::move(reason);
ready = false;
cartridge.node->setAttribute("error", publishedError);
}
auto clearError() -> void {
publishedError = {};
cartridge.node->setAttribute("error");
}
auto publishRuntimeError() -> bool {
if(!runtime.faulted()) return false;
auto error = runtime.error();
if(error != publishedError) {
publishedError = std::move(error);
cartridge.node->setAttribute("error", publishedError);
}
return true;
}
auto access(n16 address, n8 value) -> n8 {
if(!ready || publishRuntimeError()) return value;
return runtime.access(address, value);
}
auto load() -> void override {
image.clear();
object = {};
linker = {};
ready = false;
clearError();
if(auto fp = pak->read("program.elf")) {
if(fp->size() > ELF::MaximumImageSize) return fail("ELF image exceeds 16 MiB");
image.resize(fp->size());
fp->read({image.data(), image.size()});
} else {
return fail("ELF package is missing program.elf");
}
if(!object.parse(image)) return fail(object.error());
if(!linker.link(object, ELF::externalSymbols(Region::NTSC()))) return fail(linker.error());
ready = true;
}
auto unload() -> void override {
runtime.unload();
image.clear();
object = {};
linker = {};
ready = false;
publishedError = {};
}
auto power(bool) -> void override {
if(!ready) return;
if(!linker.link(object, ELF::externalSymbols(Region::NTSC()))) return fail(linker.error());
runtime.power(linker.result(), Region::NTSC());
clearError();
publishRuntimeError();
}
auto read(n16 address, n8 value) -> n8 override {
return access(address, value);
}
auto write(n16 address, n8 value) -> n8 override {
return access(address, value);
}
auto serialize(serializer& s) -> void override {
if(!ready) return;
runtime.serialize(s);
if(s.reading()) {
clearError();
publishRuntimeError();
}
}
std::vector<u8> image;
ELF::Object object;
ELF::Linker linker;
ELF::Runtime runtime;
bool ready = false;
string publishedError;
};
+31
View File
@@ -0,0 +1,31 @@
struct Enhanced3F : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n8 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
if(!address.bit(11)) return rom.read(bank * 0x800 + (address & 0x07ff));
return rom.read(rom.size() - 0x800 + (address & 0x07ff));
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address <= 0x003f) bank = data % (rom.size() >> 11);
return data;
}
auto power(bool reset) -> void override {
//Stella powers bank 0; MAME powers the final bank.
bank = 0;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
};
+118
View File
@@ -0,0 +1,118 @@
struct FA2 : Interface, Thread {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
PersistentMemory persistent;
n3 bank;
n9 busyTicks;
n1 operationCompleted;
n1 operationSucceeded;
auto load() -> void override {
Memory::Readable<n8> image;
Interface::load(image, "program.rom");
if(image.size() >= 29_KiB) {
rom.allocate(28_KiB, 0x00);
for(u32 offset : range(28_KiB)) rom.program(offset, image.read(1_KiB + offset));
} else {
rom.allocate(image.size(), 0x00);
for(u32 offset : range(image.size())) rom.program(offset, image.read(offset));
}
persistent.load(pak, "save.flash", 256, 0x00);
}
auto save() -> void override {
persistent.flush(pak);
}
auto unload() -> void override {
persistent.flush(pak);
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
if(address == 0x1ff4 && rom.size() == 28_KiB) return operate();
bankswitch(address);
if(address <= 0x10ff) {
ram.write(address & 0xff, data);
return data;
}
if(address <= 0x11ff) return ram.read(address & 0xff);
return rom.read(bank * 4_KiB + (address & 0x0fff));
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
if(address == 0x1ff4 && rom.size() == 28_KiB) {
operate();
return data;
}
bankswitch(address);
if(address <= 0x10ff) ram.write(address & 0xff, data);
return data;
}
auto main() -> void {
if(busyTicks && !--busyTicks) {
operationCompleted = 1;
ram.write(255, operationSucceeded ? 0x00 : 0xff);
}
step(1);
Thread::synchronize(cpu);
}
auto power(bool reset) -> void override {
Thread::create(2'000, std::bind_front(&FA2::main, this));
bank = 0;
busyTicks = 0;
operationCompleted = 0;
operationSucceeded = 0;
ram.allocate(256, 0x00);
}
auto serialize(serializer& s) -> void override {
Thread::serialize(s);
s(bank);
s(busyTicks);
s(operationCompleted);
s(operationSucceeded);
s(ram);
}
private:
auto bankswitch(n16 address) -> void {
if(address < 0x1ff5 || address > 0x1ffb) return;
auto selected = address - 0x1ff5;
if(selected < rom.size() / 4_KiB) bank = selected;
}
auto operate() -> n8 {
auto value = rom.read(bank * 4_KiB + 0x0ff4);
if(busyTicks) return value | 0x40;
if(operationCompleted) {
operationCompleted = 0;
return value & ~0x40;
}
operationSucceeded = 1;
auto command = ram.read(255);
if(command == 0) {
persistent.erase();
busyTicks = 202;
} else if(command == 1) {
for(u32 offset : range(256)) ram.write(offset, persistent.read(offset));
busyTicks = 1;
} else if(command == 2) {
persistent.replace({(const u8*)ram.data(), 256});
busyTicks = 202;
} else {
operationSucceeded = 0;
ram.write(255, 0xff);
return value & ~0x40;
}
return value | 0x40;
}
};
+124
View File
@@ -0,0 +1,124 @@
struct FourA50 : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n16 sliceLow;
n16 sliceMiddle;
n16 sliceHigh;
n1 romLow;
n1 romMiddle;
n1 romHigh;
n16 lastAddress;
n8 lastData;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) {
checkBankSwitch(address, data);
} else if((address & 0x1800) == 0x1000) {
data = romLow ? readRom(sliceLow + (address & 0x07ff)) : ram.read(sliceLow + (address & 0x07ff));
} else if(address <= 0x1dff) {
data = romMiddle ? readRom(0x10000 + sliceMiddle + (address & 0x07ff))
: ram.read(sliceMiddle + (address & 0x07ff));
} else if(address <= 0x1eff) {
data = romHigh ? readRom(0x10000 + sliceHigh + (address & 0x00ff))
: ram.read(sliceHigh + (address & 0x00ff));
} else {
data = readRom(0x1ff00 + (address & 0x00ff));
if((lastData & 0xe0) == 0x60 && (lastAddress >= 0x1000 || lastAddress < 0x0200)) {
sliceHigh = (sliceHigh & 0xf0ff) | ((address & 0x0008) << 8) | ((address & 0x0070) << 4);
}
}
lastData = data;
lastAddress = address;
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) {
checkBankSwitch(address, data);
} else if((address & 0x1800) == 0x1000) {
if(!romLow) {
ram.write(sliceLow + (address & 0x07ff), data);
}
} else if(address <= 0x1dff) {
if(!romMiddle) {
ram.write(sliceMiddle + (address & 0x07ff), data);
}
} else if(address <= 0x1eff) {
if(!romHigh) {
ram.write(sliceHigh + (address & 0x00ff), data);
}
} else if((lastData & 0xe0) == 0x60 && (lastAddress >= 0x1000 || lastAddress < 0x0200)) {
sliceHigh = (sliceHigh & 0xf0ff) | ((address & 0x0008) << 8) | ((address & 0x0070) << 4);
}
lastData = data;
lastAddress = address;
return data;
}
auto power(bool reset) -> void override {
sliceLow = 0;
sliceMiddle = 0;
sliceHigh = 0;
romLow = 1;
romMiddle = 1;
romHigh = 1;
lastAddress = 0xffff;
lastData = 0xff;
ram.allocate(32_KiB, 0x00);
}
auto serialize(serializer& s) -> void override {
s(ram);
s(sliceLow);
s(sliceMiddle);
s(sliceHigh);
s(romLow);
s(romMiddle);
s(romHigh);
s(lastAddress);
s(lastData);
}
private:
auto readRom(u32 address) -> n8 {
return rom.read(address % rom.size());
}
auto bankROMLower(u16 value) -> void { romLow = 1; sliceLow = value << 11; }
auto bankRAMLower(u16 value) -> void { romLow = 0; sliceLow = value << 11; }
auto bankROMMiddle(u16 value) -> void { romMiddle = 1; sliceMiddle = value << 11; }
auto bankRAMMiddle(u16 value) -> void { romMiddle = 0; sliceMiddle = value << 11; }
auto bankROMHigh(u16 value) -> void { romHigh = 1; sliceHigh = value << 8; }
auto bankRAMHigh(u16 value) -> void { romHigh = 0; sliceHigh = value << 8; }
auto checkBankSwitch(n16 address, n8 data) -> void {
if((lastData & 0xe0) == 0x60 && (lastAddress >= 0x1000 || lastAddress < 0x0200)) {
if((address & 0x0f00) == 0x0c00) bankROMHigh(address & 0x00ff);
else if((address & 0x0f00) == 0x0d00) bankRAMHigh(address & 0x007f);
else if((address & 0x0f40) == 0x0e00) bankROMLower(address & 0x001f);
else if((address & 0x0f40) == 0x0e40) bankRAMLower(address & 0x000f);
else if((address & 0x0f40) == 0x0f00) bankROMMiddle(address & 0x001f);
else if((address & 0x0f50) == 0x0f40) bankRAMMiddle(address & 0x000f);
else if((address & 0x0f00) == 0x0400) sliceLow ^= 0x0800;
else if((address & 0x0f00) == 0x0500) sliceLow ^= 0x1000;
else if((address & 0x0f00) == 0x0800) sliceMiddle ^= 0x0800;
else if((address & 0x0f00) == 0x0900) sliceMiddle ^= 0x1000;
}
if((address & 0x0f75) == 0x0074) bankROMHigh(data);
else if((address & 0x0f75) == 0x0075) bankRAMHigh(data & 0x7f);
else if((address & 0x0f7c) == 0x0078) {
if((data & 0xf0) == 0x00) bankROMLower(data & 0x0f);
else if((data & 0xf0) == 0x40) bankRAMLower(data & 0x0f);
else if((data & 0xf0) == 0x90) bankROMMiddle((data & 0x0f) | 0x10);
else if((data & 0xf0) == 0xc0) bankRAMMiddle(data & 0x0f);
}
}
};
+38
View File
@@ -0,0 +1,38 @@
struct Jane : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n2 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
return data;
}
auto power(bool reset) -> void override {
bank = 1;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
private:
auto select(n16 address) -> void {
if(address == 0x1ff0) bank = 0;
if(address == 0x1ff1) bank = 1;
if(address == 0x1ff8) bank = 2;
if(address == 0x1ff9) bank = 3;
}
};
+38
View File
@@ -0,0 +1,38 @@
struct JVP : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n1 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
return data;
}
auto power(bool reset) -> void override {
//Stella powers bank 1; MAME powers bank 0.
bank = 0;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
private:
auto select(n16 address) -> void {
//Stella selects directly; MAME toggles either hotspot.
if((address & 0x16e0) == 0x06a0) bank = 0;
if((address & 0x16e0) == 0x06c0) bank = 1;
}
};
+6 -21
View File
@@ -1,8 +1,6 @@
struct Linear : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
bool hasRam;
auto load() -> void override {
Interface::load(rom, "program.rom");
@@ -14,32 +12,19 @@ struct Linear : Interface {
auto unload() -> void override {
}
auto read(n16 address) -> n8 override {
if(address.bit(12)) {
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
return rom.read(address & 0xfff);
}
auto read(n16 address, n8 data) -> n8 override {
if(address.bit(12)) return rom.read(address & 0xfff);
return 0xff;
return data;
}
auto write(n16 address, n8 data) -> bool override {
if(address >= 0x1000 && address <= 0x107f) {
hasRam = true;
ram.write(address & 0x7f, data);
return true;
}
return false;
auto write(n16 address, n8 data) -> n8 override {
return data;
}
auto power() -> void override {
hasRam = 0;
ram.allocate(128);
auto power(bool reset) -> void override {
}
auto serialize(serializer& s) -> void override {
s(hasRam);
s(ram);
}
};
+86
View File
@@ -0,0 +1,86 @@
struct MNetwork : Interface {
using Interface::Interface;
static constexpr u8 LowerRam = 7;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n3 lowerSelector;
n2 upperRamBank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto select(n16 address) -> void {
address &= 0x1fff;
if(address >= 0x1fe8 && address <= 0x1feb) upperRamBank = address - 0x1fe8;
if(address < 0x1fe0 || address > 0x1fe7) return;
auto selector = address - 0x1fe0;
if(selector == 7) {
lowerSelector = LowerRam;
return;
}
if(rom.size() == 8_KiB) {
if(selector >= 4) lowerSelector = selector - 4;
return;
}
if(rom.size() == 12_KiB) {
static constexpr u8 banks[] = {0, 1, 0, 1, 2, 3, 4};
lowerSelector = banks[selector];
return;
}
lowerSelector = selector;
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(address >= 0x1000 && address <= 0x13ff) {
if(lowerSelector != LowerRam) return rom.read((lowerSelector * 0x800) + (address & 0x7ff));
ram.write(address & 0x3ff, data);
return data;
}
if(address >= 0x1400 && address <= 0x17ff) {
if(lowerSelector != LowerRam) return rom.read((lowerSelector * 0x800) + (address & 0x7ff));
return ram.read(address & 0x3ff);
}
if(address >= 0x1800 && address <= 0x18ff) {
auto offset = 0x400 + upperRamBank * 0x100 + (address & 0xff);
ram.write(offset, data);
return data;
}
if(address >= 0x1900 && address <= 0x19ff) {
return ram.read(0x400 + upperRamBank * 0x100 + (address & 0xff));
}
if(address >= 0x1a00) return rom.read((rom.size() - 0x800) + (address & 0x7ff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(lowerSelector == LowerRam && address >= 0x1000 && address <= 0x13ff) {
ram.write(address & 0x3ff, data);
return data;
}
if(address >= 0x1800 && address <= 0x18ff) {
ram.write(0x400 + upperRamBank * 0x100 + (address & 0xff), data);
return data;
}
return data;
}
auto power(bool reset) -> void override {
lowerSelector = 0;
upperRamBank = 0;
ram.allocate(2_KiB, 0x00);
}
auto serialize(serializer& s) -> void override {
s(lowerSelector);
s(upperRamBank);
s(ram);
}
};
+40
View File
@@ -0,0 +1,40 @@
struct MDM : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n7 bank;
n1 bankingDisabled;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
bankswitch(address);
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
bankswitch(address & 0x1fff);
return data;
}
auto power(bool reset) -> void override {
bank = 0;
bankingDisabled = 0;
}
auto serialize(serializer& s) -> void override {
s(bank);
s(bankingDisabled);
}
private:
auto bankswitch(n16 address) -> void {
if(bankingDisabled || (address & 0x1c00) != 0x0800) return;
auto selector = address & 0x00ff;
bank = selector % (rom.size() >> 12);
if(selector > 127) bankingDisabled = 1;
}
};
+37
View File
@@ -0,0 +1,37 @@
struct MegaBoy : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n4 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
return data;
}
auto power(bool reset) -> void override {
//Stella latch bank 15.
//MAME latch bank 0.
bank = 0;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
private:
auto select(n16 address) -> void {
if(address == 0x1ff0) bank++;
}
};
+702
View File
@@ -0,0 +1,702 @@
struct MovieCart : Interface {
using Interface::Interface;
static constexpr u32 TitleCycles = 1'000'000;
static constexpr u16 StreamRoutine = 0x1080;
static constexpr u16 BlankRoutine = 0x1180;
static constexpr u16 InputRoutine = 0x1100;
static constexpr u16 DifficultyRoutine = 0x1200;
static constexpr u16 ConsoleRoutine = 0x1300;
static constexpr u16 PulseRoutine = 0x1560;
static constexpr u16 LineTrampoline = 0x1070;
struct Kernel {
std::array<u8, 1_KiB> image{};
u16 vsyncLines = 0;
u16 vblankLines = 0;
u16 overscanLines = 0;
u16 visibleDone = 0;
u16 visibleExit = 0;
u16 background = 0;
std::array<u16, 5> playfieldColor{};
u16 playfield0Left = 0;
u16 playfield1Left = 0;
u16 playfield2Left = 0;
u16 playfield0Right = 0;
u16 playfield1Right = 0;
u16 playfield2Right = 0;
std::array<u16, 2> audioVolume{};
} kernel;
VFS::File stream;
std::array<std::array<u8, MVC::FieldSize>, 2> fields{};
MVC::FieldDescriptor descriptors[2];
std::array<u32, 2> loadedFields = {~0u, ~0u};
std::array<bool, 2> blankFields{};
std::array<u8, 32> fingerprint{};
u64 streamSize = 0;
u32 fieldCount = 0;
u32 fieldNumber = 0;
u16 scanline = 0;
n1 activeBuffer = 0;
n1 odd = 1;
n1 a7 = 0;
n1 a10 = 0;
n16 a10Pulses = 0;
bool streamStarted = false;
u8 previousCommand = 0;
u8 repeatFields = 0;
u8 seekSpeed = 2;
u8 muteFields = 0;
u8 volume = 15;
u8 brightness = 15;
u8 osdMode = 0;
bool monochrome = false;
bool playing = true;
enum class TitleState : u8 { Display, Exiting, Stream } titleState = TitleState::Display;
u32 titleCycles = 0;
bool ready = false;
auto emit(u16& pc, u8 value) -> void {
kernel.image[pc++ & 0x03ff] = value;
}
auto immediate(u16& pc, u8 opcode, u8 value, u16* operand = nullptr) -> void {
emit(pc, opcode);
if(operand) *operand = pc;
emit(pc, value);
}
auto absolute(u16& pc, u8 opcode, u16 address) -> void {
emit(pc, opcode);
emit(pc, address);
emit(pc, address >> 8);
}
auto zeroPage(u16& pc, u8 opcode, u8 address) -> void {
emit(pc, opcode);
emit(pc, address);
}
auto branch(u16& pc, u8 opcode, u16 target) -> void {
emit(pc, opcode);
auto origin = pc + 1;
emit(pc, (u8)(target - origin));
}
auto branchPlaceholder(u16& pc, u8 opcode) -> u16 {
emit(pc, opcode);
auto operand = pc;
emit(pc, 0);
return operand;
}
auto patchBranch(u16 operand, u16 target) -> void {
kernel.image[operand & 0x03ff] = (u8)(target - (operand + 1));
}
auto buildKernel() -> void {
kernel = {};
kernel.image.fill(0xea);
u16 pc = 0;
emit(pc, 0x78); //SEI
emit(pc, 0xd8); //CLD
immediate(pc, 0xa2, 0xff); //LDX #$ff
emit(pc, 0x9a); //TXS
immediate(pc, 0xa9, 0x02); //LDA #$02
zeroPage(pc, 0x85, 0x01); //STA VBLANK
immediate(pc, 0xa9, 0x00); //LDA #$00
zeroPage(pc, 0x85, 0x0a); //STA CTRLPF
immediate(pc, 0xa9, 0x04); //LDA #pure tone
zeroPage(pc, 0x85, 0x15); //STA AUDC0
immediate(pc, 0xa9, 0x00); //LDA #frequency
zeroPage(pc, 0x85, 0x17); //STA AUDF0
auto frame = pc;
immediate(pc, 0xa9, 0x02); //LDA #$02
zeroPage(pc, 0x85, 0x00); //STA VSYNC
immediate(pc, 0xa2, 3, &kernel.vsyncLines); //LDX #vsync
auto vsyncLoop = pc;
absolute(pc, 0x20, BlankRoutine); //JSR blank line
emit(pc, 0xca); //DEX
branch(pc, 0xd0, vsyncLoop); //BNE
immediate(pc, 0xa9, 0x00); //LDA #0
zeroPage(pc, 0x85, 0x00); //STA VSYNC
immediate(pc, 0xa2, 37, &kernel.vblankLines);//LDX #vblank
auto vblankLoop = pc;
absolute(pc, 0x20, BlankRoutine); //JSR blank line
emit(pc, 0xca); //DEX
branch(pc, 0xd0, vblankLoop); //BNE
immediate(pc, 0xa9, 0x00); //LDA #0
zeroPage(pc, 0x85, 0x01); //STA VBLANK
absolute(pc, 0x4c, StreamRoutine); //JMP visible stream
auto visibleDone = pc;
kernel.visibleExit = visibleDone + 0x1000;
immediate(pc, 0xa9, 0x02); //LDA #2
zeroPage(pc, 0x85, 0x01); //STA VBLANK
absolute(pc, 0x20, InputRoutine); //JSR input sampler
immediate(pc, 0xa2, 30, &kernel.overscanLines);//LDX #overscan
auto overscanLoop = pc;
absolute(pc, 0x20, BlankRoutine); //JSR blank line
emit(pc, 0xca); //DEX
branch(pc, 0xd0, overscanLoop); //BNE
absolute(pc, 0x4c, 0x1000 + frame); //JMP frame
pc = LineTrampoline & 0x03ff;
emit(pc, 0x4c); //JMP visible stream/done
kernel.visibleDone = pc;
emit(pc, StreamRoutine & 0xff);
emit(pc, StreamRoutine >> 8);
pc = PulseRoutine & 0x03ff;
emit(pc, 0x60); //RTS (one A10 pulse)
pc = StreamRoutine & 0x03ff;
zeroPage(pc, 0x85, 0x02); //STA WSYNC
immediate(pc, 0xa9, 0x00, &kernel.background);
zeroPage(pc, 0x85, 0x09); //STA COLUBK
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[0]);
zeroPage(pc, 0x85, 0x08); //STA COLUPF
immediate(pc, 0xa9, 0x00, &kernel.playfield0Left);
zeroPage(pc, 0x85, 0x0d); //STA PF0
immediate(pc, 0xa9, 0x00, &kernel.playfield1Left);
zeroPage(pc, 0x85, 0x0e); //STA PF1
immediate(pc, 0xa9, 0x00, &kernel.playfield2Left);
zeroPage(pc, 0x85, 0x0f); //STA PF2
immediate(pc, 0xa9, 0x00, &kernel.audioVolume[0]);
zeroPage(pc, 0x85, 0x19); //STA AUDV0
emit(pc, 0xea); //align five color cells
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[1]);
zeroPage(pc, 0x85, 0x08); //STA COLUPF
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[2]);
zeroPage(pc, 0x85, 0x08); //STA COLUPF
immediate(pc, 0xa9, 0x00, &kernel.playfield0Right);
zeroPage(pc, 0x85, 0x0d); //STA PF0
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[3]);
zeroPage(pc, 0x85, 0x08); //STA COLUPF
immediate(pc, 0xa9, 0x00, &kernel.playfield1Right);
zeroPage(pc, 0x85, 0x0e); //STA PF1
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[4]);
zeroPage(pc, 0x85, 0x08); //STA COLUPF
immediate(pc, 0xa9, 0x00, &kernel.playfield2Right);
zeroPage(pc, 0x85, 0x0f); //STA PF2
absolute(pc, 0x4c, LineTrampoline); //JMP low-page trampoline
pc = BlankRoutine & 0x03ff;
zeroPage(pc, 0x85, 0x02); //STA WSYNC
immediate(pc, 0xa9, 0x00, &kernel.audioVolume[1]);
zeroPage(pc, 0x85, 0x19); //STA AUDV0
emit(pc, 0x60); //RTS
pc = InputRoutine & 0x03ff;
zeroPage(pc, 0xa5, 0x0c); //LDA INPT4
auto noFire = branchPlaceholder(pc, 0x30); //BMI not pressed
absolute(pc, 0x20, PulseRoutine);
emit(pc, 0x60); //fire = command 1
patchBranch(noFire, pc);
auto joystickCommand = [&](u8 mask, u8 pulses) {
absolute(pc, 0xad, 0x0280); //LDA SWCHA
immediate(pc, 0x29, mask); //AND direction
auto notPressed = branchPlaceholder(pc, 0xd0);
for(u32 pulse : range(pulses)) absolute(pc, 0x20, PulseRoutine);
emit(pc, 0x60);
patchBranch(notPressed, pc);
};
joystickCommand(0x40, 2); //left = seek backward
joystickCommand(0x80, 3); //right = seek forward
joystickCommand(0x10, 4); //up = volume up
joystickCommand(0x20, 5); //down = volume down
absolute(pc, 0x4c, DifficultyRoutine); //JMP difficulty sampler
pc = DifficultyRoutine & 0x03ff;
auto difficultyCommand = [&](u8 mask, u8 pulses) {
absolute(pc, 0xad, 0x0282); //LDA SWCHB
immediate(pc, 0x29, mask); //AND difficulty switch
auto inactive = branchPlaceholder(pc, 0xd0);
for(u32 pulse : range(pulses)) absolute(pc, 0x20, PulseRoutine);
emit(pc, 0x60);
patchBranch(inactive, pc);
};
difficultyCommand(0x40, 9); //left difficulty = ten seconds back
difficultyCommand(0x80, 10); //right difficulty = single step
absolute(pc, 0x4c, ConsoleRoutine); //JMP console sampler
pc = ConsoleRoutine & 0x03ff;
auto consoleCommand = [&](u8 mask, u8 pulses) {
absolute(pc, 0xad, 0x0282); //LDA SWCHB
immediate(pc, 0x29, mask); //AND console switch
auto inactive = branchPlaceholder(pc, 0xd0);
for(u32 pulse : range(pulses)) absolute(pc, 0x20, PulseRoutine);
emit(pc, 0x60);
patchBranch(inactive, pc);
};
consoleCommand(0x02, 6); //select = cycle OSD
consoleCommand(0x01, 7); //reset = restart movie
consoleCommand(0x08, 8); //B/W = monochrome toggle
emit(pc, 0x60);
kernel.image[0x03fc] = 0x00;
kernel.image[0x03fd] = 0x10;
kernel.image[0x03fe] = 0x00;
kernel.image[0x03ff] = 0x10;
}
auto fail(string reason) -> void {
ready = false;
cartridge.node->setAttribute("error", reason);
}
auto updateStatus() -> void {
cartridge.node->setAttribute("moviecart-field", string{fieldNumber});
cartridge.node->setAttribute("moviecart-state", playing ? "playing" : "paused");
cartridge.node->setAttribute("moviecart-volume", string{volume});
cartridge.node->setAttribute("moviecart-brightness", string{brightness});
cartridge.node->setAttribute("moviecart-monochrome", monochrome ? "on" : "off");
cartridge.node->setAttribute("moviecart-osd", string{osdMode});
cartridge.node->setAttribute("moviecart-seek-speed", string{seekSpeed});
cartridge.node->setAttribute("moviecart-repeat", string{repeatFields});
cartridge.node->setAttribute("moviecart-mute", string{muteFields});
static constexpr const char* titleNames[] = {"display", "exiting", "stream"};
cartridge.node->setAttribute("moviecart-title", titleNames[(u32)titleState]);
}
auto readField(u32 number, u32 buffer) -> bool {
if(!stream || number >= fieldCount || buffer >= fields.size()) return false;
auto offset = (u64)number * MVC::FieldSize;
if(offset > streamSize || MVC::FieldSize > streamSize - offset) return false;
std::array<u8, MVC::FieldSize> candidate{};
stream->seek(offset);
stream->read({candidate.data(), candidate.size()});
MVC::FieldDescriptor descriptor;
if(!MVC::parseField(candidate, descriptor)) {
loadedFields[buffer] = number;
blankFields[buffer] = true;
return false;
}
fields[buffer] = candidate;
descriptors[buffer] = descriptor;
loadedFields[buffer] = number;
blankFields[buffer] = false;
return true;
}
auto calculateFingerprint() -> void {
Hash::SHA256 hash;
MVC::fingerprintPrefix(hash, streamSize, fields[0], fields[1]);
stream->seek(streamSize - MVC::FieldSize);
for(u32 byte : range(MVC::FieldSize)) hash.input(stream->read());
auto output = hash.output();
for(u32 index : range(fingerprint.size())) fingerprint[index] = output[index];
}
auto load() -> void override {
cartridge.node->setAttribute("error");
stream = pak->read("stream.mvc");
if(!stream) return fail("MovieCart package is missing stream.mvc");
streamSize = stream->size();
if(streamSize < MVC::MinimumSize || streamSize % MVC::FieldSize) return fail("invalid MovieCart stream size");
if(streamSize / MVC::FieldSize > 0xffffffffull) return fail("MovieCart stream has too many fields");
fieldCount = streamSize / MVC::FieldSize;
if(!readField(0, 0) || !readField(1, 1)) return fail("invalid initial MovieCart field");
calculateFingerprint();
ready = true;
}
auto unload() -> void override {
stream.reset();
for(auto& field : fields) field.fill(0);
descriptors[0] = {};
descriptors[1] = {};
loadedFields = {~0u, ~0u};
blankFields = {};
fingerprint.fill(0);
streamSize = 0;
fieldCount = 0;
ready = false;
}
auto activateField() -> void {
auto& descriptor = descriptors[(u32)activeBuffer];
scanline = 0;
if(!descriptor.valid) return;
odd = descriptor.embeddedFrame & 1;
kernel.image[kernel.vsyncLines] = descriptor.vsync;
kernel.image[kernel.vblankLines] = descriptor.vblank;
kernel.image[kernel.overscanLines] = descriptor.overscan;
kernel.image[kernel.visibleDone + 0] = StreamRoutine & 0xff;
kernel.image[kernel.visibleDone + 1] = StreamRoutine >> 8;
if(blankFields[(u32)activeBuffer]) {
cartridge.node->setAttribute("error", string{"invalid MovieCart field ", fieldNumber});
} else {
cartridge.node->setAttribute("error");
}
patchLeft();
}
auto clearLeft() -> void {
kernel.image[kernel.background] = 0;
for(u32 cell : range(3)) kernel.image[kernel.playfieldColor[cell]] = 0;
kernel.image[kernel.playfield0Left] = 0;
kernel.image[kernel.playfield1Left] = 0;
kernel.image[kernel.playfield2Left] = 0;
}
auto clearRight() -> void {
for(u32 cell : range(3, 5)) kernel.image[kernel.playfieldColor[cell]] = 0;
kernel.image[kernel.playfield0Right] = 0;
kernel.image[kernel.playfield1Right] = 0;
kernel.image[kernel.playfield2Right] = 0;
}
static auto reverseByte(u8 value) -> u8 {
value = (value & 0x55) << 1 | (value & 0xaa) >> 1;
value = (value & 0x33) << 2 | (value & 0xcc) >> 2;
return value << 4 | value >> 4;
}
static auto reverseNybble(u8 value) -> u8 {
value &= 15;
return ((value & 1) << 3) | ((value & 2) << 1) | ((value & 4) >> 1) | ((value & 8) >> 3);
}
auto adjustColor(u8 color) const -> u8 {
auto luminance = ((color & 0x0e) * brightness / 15) & 0x0e;
return (monochrome ? 0 : color & 0xf0) | luminance;
}
static auto titlePixel(u32 x, u32 y) -> bool {
auto letterM = (x == 2 || x == 11) || (y < 5 && (x == 2 + y || x == 11 - y));
auto letterV = x == 15 + y / 2 || x == 24 - y / 2;
auto letterC = (x == 28 && y > 0 && y < 11) || ((y == 0 || y == 11) && x >= 29 && x <= 37);
return letterM || letterV || letterC;
}
auto titleByte(u32 visibleLine, u32 cell, u32 visibleLines) const -> u8 {
static constexpr u32 height = 12;
auto presentationLines = Region::NTSC() ? 192u : 228u;
auto top = (presentationLines - height) / 2;
if(visibleLines < top + height) top = visibleLines > height ? visibleLines - height : 0;
if(visibleLine < top || visibleLine >= top + height) return 0;
auto y = visibleLine - top;
u8 output = 0;
for(u32 bit : range(8)) if(titlePixel(cell * 8 + bit, y)) output |= 0x80 >> bit;
return output;
}
auto patchLeft() -> void {
auto buffer = (u32)activeBuffer;
auto& descriptor = descriptors[buffer];
if(blankFields[buffer] || !descriptor.valid || scanline >= descriptor.audioCount) {
clearLeft();
for(auto operand : kernel.audioVolume) kernel.image[operand] = 0;
return;
}
auto visibleStart = (u32)descriptor.vsync + descriptor.vblank;
if(titleState != TitleState::Stream) {
for(auto operand : kernel.audioVolume) kernel.image[operand] = 0;
if(scanline < visibleStart || scanline >= visibleStart + descriptor.visible) return clearLeft();
auto visibleLine = scanline - visibleStart;
auto graph0 = titleByte(visibleLine, 0, descriptor.visible);
auto graph1 = titleByte(visibleLine, 1, descriptor.visible);
auto graph2 = titleByte(visibleLine, 2, descriptor.visible);
kernel.image[kernel.background] = adjustColor(((visibleLine / 12 + 1) & 15) << 4 | 0x02);
for(u32 cell : range(3)) kernel.image[kernel.playfieldColor[cell]] = adjustColor(0x0e);
kernel.image[kernel.playfield0Left] = reverseNybble(graph0 >> 4) << 4;
kernel.image[kernel.playfield1Left] = graph0 << 4 | graph1 >> 4;
kernel.image[kernel.playfield2Left] = reverseByte(graph1 << 4 | graph2 >> 4);
return;
}
auto& field = fields[buffer];
auto audio = muteFields ? 0 : (field[descriptor.audioOffset + scanline] & 15) * volume / 15;
for(auto operand : kernel.audioVolume) kernel.image[operand] = audio;
if(scanline < visibleStart || scanline >= visibleStart + descriptor.visible) {
clearLeft();
return;
}
auto visibleLine = scanline - visibleStart;
auto overlay = osdMode && visibleLine < descriptor.timecodeCount / 5;
auto graph = overlay ? descriptor.timecodeOffset + visibleLine * 5
: descriptor.graphOffset + visibleLine * 5;
auto color = descriptor.colorOffset + visibleLine * 5;
auto backgroundLine = min<u32>(visibleLine + !odd, descriptor.backgroundCount - 1);
auto graph0 = field[graph + 0];
auto graph1 = field[graph + 1];
auto graph2 = field[graph + 2];
kernel.image[kernel.background] = overlay ? 0 : adjustColor(field[descriptor.backgroundOffset + backgroundLine]);
for(u32 cell : range(3)) {
kernel.image[kernel.playfieldColor[cell]] = overlay ? adjustColor(0x0e) : adjustColor(field[color + cell]);
}
kernel.image[kernel.playfield0Left] = reverseNybble(graph0 >> 4) << 4;
kernel.image[kernel.playfield1Left] = graph0 << 4 | graph1 >> 4;
kernel.image[kernel.playfield2Left] = reverseByte(graph1 << 4 | graph2 >> 4);
}
auto patchRight() -> void {
auto buffer = (u32)activeBuffer;
auto& descriptor = descriptors[buffer];
auto visibleStart = (u32)descriptor.vsync + descriptor.vblank;
if(blankFields[buffer] || !descriptor.valid || scanline < visibleStart
|| scanline >= visibleStart + descriptor.visible) {
clearRight();
return;
}
if(titleState != TitleState::Stream) {
auto visibleLine = scanline - visibleStart;
auto graph2 = titleByte(visibleLine, 2, descriptor.visible);
kernel.image[kernel.playfield0Right] = reverseNybble(graph2) << 4;
kernel.image[kernel.playfield1Right] = titleByte(visibleLine, 3, descriptor.visible);
kernel.image[kernel.playfield2Right] = reverseByte(titleByte(visibleLine, 4, descriptor.visible));
kernel.image[kernel.playfieldColor[3]] = adjustColor(0x0e);
kernel.image[kernel.playfieldColor[4]] = adjustColor(0x0e);
return;
}
auto& field = fields[buffer];
auto visibleLine = scanline - visibleStart;
auto overlay = osdMode && visibleLine < descriptor.timecodeCount / 5;
auto graph = overlay ? descriptor.timecodeOffset + visibleLine * 5
: descriptor.graphOffset + visibleLine * 5;
auto color = descriptor.colorOffset + visibleLine * 5;
auto graph2 = field[graph + 2];
kernel.image[kernel.playfield0Right] = reverseNybble(graph2) << 4;
kernel.image[kernel.playfield1Right] = field[graph + 3];
kernel.image[kernel.playfield2Right] = reverseByte(field[graph + 4]);
kernel.image[kernel.playfieldColor[3]] = overlay ? adjustColor(0x0e) : adjustColor(field[color + 3]);
kernel.image[kernel.playfieldColor[4]] = overlay ? adjustColor(0x0e) : adjustColor(field[color + 4]);
}
auto advanceField() -> void {
if(titleState != TitleState::Stream) {
a10Pulses = 0;
previousCommand = 0;
fieldNumber = 0;
activeBuffer = loadedFields[0] == 0 ? 0 : 1;
activateField();
updateStatus();
return;
}
auto command = min<u32>(a10Pulses, 10);
a10Pulses = 0;
if(muteFields) muteFields--;
s32 seek = 0;
bool restart = false;
bool step = false;
auto fresh = command != previousCommand;
if(command == 2 || command == 3) {
if(fresh) {
repeatFields = 0;
seekSpeed = 2;
} else {
if(repeatFields < 255) repeatFields++;
if(repeatFields && repeatFields % 30 == 0) seekSpeed = min<u32>(seekSpeed * 2, 64);
}
seek = command == 2 ? -(s32)seekSpeed : +(s32)seekSpeed;
} else {
repeatFields = 0;
seekSpeed = 2;
}
if(fresh) {
if(command == 1) playing = !playing;
if(command == 4) {
if(osdMode == 2 && brightness < 15) brightness++;
else if(osdMode != 2 && volume < 15) volume++;
}
if(command == 5) {
if(osdMode == 2 && brightness > 0) brightness--;
else if(osdMode != 2 && volume > 0) volume--;
}
if(command == 6) osdMode = (osdMode + 1) % 3;
if(command == 7) {
restart = true;
playing = true;
}
if(command == 8) monochrome = !monochrome;
if(command == 9) seek = -(s32)descriptors[(u32)activeBuffer].rate * 10;
if(command == 10) {
playing = false;
step = true;
}
}
previousCommand = command;
s64 requested = fieldNumber;
if(restart) requested = 0;
else if(seek) requested += seek;
else if(step) requested++;
else if(playing) requested++;
while(requested < 0) requested += 2;
auto reachedEOF = requested >= fieldCount;
while(requested >= fieldCount) requested -= 2;
if(reachedEOF) {
repeatFields = 0;
seekSpeed = 2;
muteFields = 2;
}
auto next = (u32)requested;
auto targetBuffer = next == fieldNumber ? (u32)activeBuffer : (u32)!activeBuffer;
if(loadedFields[targetBuffer] != next) readField(next, targetBuffer);
fieldNumber = next;
activeBuffer = targetBuffer;
auto lookahead = fieldNumber + 1;
if(lookahead >= fieldCount) lookahead = fieldCount - 2;
auto lookaheadBuffer = (u32)!activeBuffer;
if(loadedFields[lookaheadBuffer] != lookahead) readField(lookahead, lookaheadBuffer);
activateField();
updateStatus();
}
auto process(n16 address) -> void {
if(titleState == TitleState::Display && ++titleCycles >= TitleCycles) titleState = TitleState::Exiting;
auto nextA10 = address.bit(10);
if(!a10 && nextA10 && !address.bit(7)) a10Pulses++;
a10 = nextA10;
auto nextA7 = address.bit(7);
if(!streamStarted) {
auto routine = address & 0x03ff;
if(nextA7 && (routine == (StreamRoutine & 0x03ff) || routine == (BlankRoutine & 0x03ff))) {
streamStarted = true;
patchRight();
}
a7 = nextA7;
return;
}
if(titleState == TitleState::Exiting && !a7 && nextA7) {
titleState = TitleState::Stream;
fieldNumber = 0;
activeBuffer = loadedFields[0] == 0 ? 0 : 1;
activateField();
updateStatus();
}
if(!a7 && nextA7) patchRight();
if(a7 && !nextA7) {
scanline++;
auto& descriptor = descriptors[(u32)activeBuffer];
auto totalLines = (u32)descriptor.vsync + descriptor.vblank + descriptor.visible + descriptor.overscan;
if(!descriptor.valid || scanline >= totalLines) advanceField();
else {
patchLeft();
auto visibleEnd = (u32)descriptor.vsync + descriptor.vblank + descriptor.visible;
if(scanline == visibleEnd) {
kernel.image[kernel.visibleDone + 0] = kernel.visibleExit & 0xff;
kernel.image[kernel.visibleDone + 1] = kernel.visibleExit >> 8;
}
}
}
a7 = nextA7;
}
auto power(bool) -> void override {
if(!ready) return;
buildKernel();
if(!readField(0, 0) || !readField(1, 1)) return fail("MovieCart stream changed while attached");
fieldNumber = 0;
activeBuffer = 0;
odd = 0;
a7 = 0;
a10 = 0;
a10Pulses = 0;
streamStarted = false;
previousCommand = 0;
repeatFields = 0;
seekSpeed = 2;
muteFields = 0;
volume = 15;
brightness = 15;
osdMode = 0;
monochrome = false;
playing = true;
titleState = TitleState::Display;
titleCycles = 0;
activateField();
updateStatus();
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!ready || !address.bit(12)) return data;
process(address);
return kernel.image[address & 0x03ff];
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(ready && address.bit(12)) process(address);
return data;
}
auto serializeDescriptor(serializer& s, MVC::FieldDescriptor& descriptor) -> void {
s(descriptor.valid);
s(descriptor.extended);
s(descriptor.signatureOffset);
s(descriptor.vsync);
s(descriptor.vblank);
s(descriptor.overscan);
s(descriptor.visible);
s(descriptor.rate);
s(descriptor.embeddedFrame);
s(descriptor.audioOffset);
s(descriptor.audioCount);
s(descriptor.graphOffset);
s(descriptor.graphCount);
s(descriptor.colorOffset);
s(descriptor.colorCount);
s(descriptor.backgroundOffset);
s(descriptor.backgroundCount);
s(descriptor.timecodeOffset);
s(descriptor.timecodeCount);
}
auto serialize(serializer& s) -> void override {
if(!ready) return;
auto savedSize = streamSize;
auto savedFingerprint = fingerprint;
s(std::span<u8>{kernel.image.data(), kernel.image.size()});
for(auto& field : fields) s(std::span<u8>{field.data(), field.size()});
serializeDescriptor(s, descriptors[0]);
serializeDescriptor(s, descriptors[1]);
for(auto& loadedField : loadedFields) s(loadedField);
for(auto& blankField : blankFields) s(blankField);
s(std::span<u8>{savedFingerprint.data(), savedFingerprint.size()});
s(savedSize);
s(fieldNumber);
s(scanline);
s(activeBuffer);
s(odd);
s(a7);
s(a10);
s(a10Pulses);
s(streamStarted);
s(previousCommand);
s(repeatFields);
s(seekSpeed);
s(muteFields);
s(volume);
s(brightness);
s(osdMode);
s(monochrome);
s(playing);
s(titleState);
s(titleCycles);
if(s.reading()) {
if(savedSize != streamSize || savedFingerprint != fingerprint) return fail("MovieCart state media mismatch");
MVC::FieldDescriptor parsed[2];
if(!MVC::parseField(fields[0], parsed[0]) || !MVC::parseField(fields[1], parsed[1])
|| parsed[0] != descriptors[0] || parsed[1] != descriptors[1]) {
return fail("invalid MovieCart field state");
}
auto& descriptor = descriptors[(u32)activeBuffer];
auto totalLines = (u32)descriptor.vsync + descriptor.vblank + descriptor.visible + descriptor.overscan;
if(fieldNumber >= fieldCount || loadedFields[0] >= fieldCount || loadedFields[1] >= fieldCount
|| loadedFields[(u32)activeBuffer] != fieldNumber || scanline >= totalLines
|| previousCommand > 10 || !seekSpeed || seekSpeed > 64 || volume > 15 || brightness > 15 || osdMode > 2
|| (u32)titleState > (u32)TitleState::Stream || titleCycles > TitleCycles) {
return fail("invalid MovieCart cursor state");
}
updateStatus();
}
}
};
@@ -0,0 +1,48 @@
struct ParkerBros03E0 : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n3 chunks[3];
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(address.bit(12)) {
u32 segment = address.bit(10, 11);
u32 chunk = 7;
if(segment < 3) chunk = chunks[segment];
return rom.read(chunk * 0x400 + (address & 0x03ff));
}
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
return data;
}
auto power(bool reset) -> void override {
chunks[0] = 4;
chunks[1] = 5;
chunks[2] = 6;
}
auto serialize(serializer& s) -> void override {
s(chunks[0]);
s(chunks[1]);
s(chunks[2]);
}
private:
auto select(n16 address) -> void {
if(address < 0x0380 || address > 0x03ff) return;
auto chunk = address & 7;
if(!address.bit(4)) chunks[0] = chunk;
if(!address.bit(5)) chunks[1] = chunk;
if(!address.bit(6)) chunks[2] = chunk;
}
};
+5 -5
View File
@@ -13,7 +13,7 @@ struct ParkerBros : Interface {
auto unload() -> void override {
}
auto read(n16 address) -> n8 override {
auto read(n16 address, n8 data) -> n8 override {
bankswitch(address);
if(address.bit(12)) {
@@ -23,12 +23,12 @@ struct ParkerBros : Interface {
if(address >= 0x1c00 && address <= 0x1fff) return rom.read(( 7 * 0x400) + (address & 0x3ff));
}
return 0xff;
return data;
}
auto write(n16 address, n8 data) -> bool override {
auto write(n16 address, n8 data) -> n8 override {
bankswitch(address);
return false;
return data;
}
auto bankswitch(n16 address) -> void {
@@ -37,7 +37,7 @@ struct ParkerBros : Interface {
if(address >= 0x1ff0 && address <= 0x1ff7) banks[2] = address - 0x1ff0;
}
auto power() -> void override {
auto power(bool reset) -> void override {
banks[0] = 0;
banks[1] = 1;
banks[2] = 2;
+32
View File
@@ -0,0 +1,32 @@
struct SaraRam {
SaraRam(bool present = false) : present(present) {}
auto readable(n16 address) -> bool {
return present && address >= 0x1000 && address <= 0x10ff;
}
auto read(n16 address, n8 data) -> n8 {
if(address <= 0x107f) {
ram.write(address & 0x7f, data);
return data;
}
return ram.read(address & 0x7f);
}
auto write(n16 address, n8 data) -> bool {
if(!present || address < 0x1000 || address > 0x107f) return false;
ram.write(address & 0x7f, data);
return true;
}
auto power() -> void {
if(present) ram.allocate(128, 0x00);
}
auto serialize(serializer& s) -> void {
if(present) s(ram);
}
const bool present;
Memory::Writable<n8> ram;
};
+34
View File
@@ -0,0 +1,34 @@
struct Superbanking : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n6 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
select(address);
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
select(address & 0x1fff);
return data;
}
auto power(bool reset) -> void override {
bank = (rom.size() >> 12) - 1;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
private:
auto select(n16 address) -> void {
if((address & 0x1800) == 0x0800) bank = address & ((rom.size() >> 12) - 1);
}
};
+60
View File
@@ -0,0 +1,60 @@
struct ThreeEPlus : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n1 ramSelected[4];
n6 bank[4];
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
auto segment = address.bit(10, 11);
auto offset = address & 0x03ff;
if(!ramSelected[segment]) return rom.read((u32)(bank[segment] % (rom.size() >> 10)) * 0x400 + offset);
auto ramOffset = (u32)bank[segment] * 0x200 + (offset & 0x01ff);
if(!(offset & 0x0200)) return ram.read(ramOffset);
ram.write(ramOffset, data);
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address == 0x003e || address == 0x003f) {
auto segment = data.bit(6, 7);
ramSelected[segment] = address == 0x003e;
bank[segment] = data.bit(0, 5);
return data;
}
if(!address.bit(12)) return data;
auto segment = address.bit(10, 11);
auto offset = address & 0x03ff;
if(ramSelected[segment] && (offset & 0x0200)) {
ram.write((u32)bank[segment] * 0x200 + (offset & 0x01ff), data);
return data;
}
return data;
}
auto power(bool reset) -> void override {
for(auto segment : range(4)) {
ramSelected[segment] = 0;
bank[segment] = 0;
}
ram.allocate(32_KiB, 0x00);
}
auto serialize(serializer& s) -> void override {
for(auto segment : range(4)) {
s(ramSelected[segment]);
s(bank[segment]);
}
s(ram);
}
};
+56
View File
@@ -0,0 +1,56 @@
struct ThreeE : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n1 ramSelected;
n8 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
if(address.bit(11)) return rom.read(rom.size() - 0x800 + (address & 0x07ff));
if(!ramSelected) return rom.read(bank * 0x800 + (address & 0x07ff));
auto offset = bank * 0x400 + (address & 0x03ff);
if(!address.bit(10)) return ram.read(offset);
ram.write(offset, data);
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address == 0x003e) {
ramSelected = 1;
bank = data & 31;
return data;
}
if(address == 0x003f) {
ramSelected = 0;
bank = data % (rom.size() >> 11);
return data;
}
if(ramSelected && address >= 0x1400 && address <= 0x17ff) {
ram.write(bank * 0x400 + (address & 0x03ff), data);
return data;
}
return data;
}
auto power(bool reset) -> void override {
//Stella powers bank 0 and MAME the final bank.
//Boulder Dash Demo 2 selects bank 1 before using the lower window.
ramSelected = 0;
bank = 1;
ram.allocate(32_KiB, 0x00);
}
auto serialize(serializer& s) -> void override {
s(ramSelected);
s(bank);
s(ram);
}
};
+57
View File
@@ -0,0 +1,57 @@
struct ThreeEX : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n1 ramSelected;
n8 bank;
u16 ramBankCount = 1;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(!address.bit(12)) return data;
if(address.bit(11)) return rom.read(rom.size() - 0x800 + (address & 0x07ff));
if(!ramSelected) return rom.read(bank * 0x800 + (address & 0x07ff));
auto offset = bank * 0x400 + (address & 0x03ff);
if(!address.bit(10)) return ram.read(offset);
ram.write(offset, data);
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
if(address == 0x003e) {
ramSelected = 1;
bank = data % ramBankCount;
return data;
}
if(address == 0x003f) {
ramSelected = 0;
bank = data % (rom.size() >> 11);
return data;
}
if(ramSelected && address >= 0x1400 && address <= 0x17ff) {
ram.write(bank * 0x400 + (address & 0x03ff), data);
return data;
}
return data;
}
auto power(bool reset) -> void override {
ramSelected = 0;
bank = 1;
ramBankCount = (u16)rom.read(rom.size() - 6);
ramBankCount++;
ram.allocate(ramBankCount * 1_KiB, 0x00);
}
auto serialize(serializer& s) -> void override {
s(ramSelected);
s(bank);
s(ram);
}
};
+5 -5
View File
@@ -14,23 +14,23 @@ struct Tigervision : Interface {
auto unload() -> void override {
}
auto read(n16 address) -> n8 override {
auto read(n16 address, n8 data) -> n8 override {
if(address.bit(12)) {
if(bank != nextBank) bank = nextBank;
if(address <= 0x17ff) return rom.read((bank * 0x800) + (address & 0x7ff));
return rom.read((rom.size() - 0x800) + (address & 0x7ff));
}
return 0xff;
return data;
}
auto write(n16 address, n8 data) -> bool override {
auto write(n16 address, n8 data) -> n8 override {
if(address.bit(12) && bank != nextBank) bank = nextBank;
if(address <= 0x3f) nextBank = data;
return false;
return data;
}
auto power() -> void override {
auto power(bool reset) -> void override {
bank = 0;
nextBank = 0;
}
+41
View File
@@ -0,0 +1,41 @@
struct UA8k : Interface {
UA8k(Cartridge& cartridge, bool swapped = false) : Interface(cartridge), swapped(swapped) {}
Memory::Readable<n8> rom;
const bool swapped;
n1 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto save() -> void override {
}
auto unload() -> void override {
}
auto read(n16 address, n8 data) -> n8 override {
bankswitch(address);
if(address.bit(12)) return rom.read((bank * 0x1000) + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
bankswitch(address);
return data;
}
auto power(bool reset) -> void override {
bank = 0;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
private:
auto bankswitch(n16 address) -> void {
if((address & 0x1260) == 0x0220) bank = swapped ? 1 : 0;
if((address & 0x1260) == 0x0240) bank = swapped ? 0 : 1;
}
};
+86
View File
@@ -0,0 +1,86 @@
struct Wickstead : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
Memory::Writable<n8> ram;
n3 arrangement;
n3 pendingArrangement;
u8 delay;
bool pending;
static constexpr u8 arrangements[8][4] = {
{0, 0, 1, 3},
{0, 1, 2, 3},
{4, 5, 6, 7},
{7, 4, 2, 3},
{0, 0, 6, 7},
{0, 1, 7, 6},
{2, 3, 4, 5},
{6, 0, 5, 1},
};
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
advance(true);
if(address >= 0x0030 && address <= 0x003f) {
pendingArrangement = address & 7;
delay = 3;
pending = true;
return data;
}
if(address >= 0x1000 && address <= 0x103f) return ram.read(address & 0x3f);
if(address >= 0x1040 && address <= 0x107f) {
ram.write(address & 0x3f, data);
return data;
}
if(address.bit(12)) {
auto segment = (address >> 10) & 3;
auto chunk = arrangements[arrangement][segment];
return rom.read(chunk * 1_KiB + (address & 0x3ff));
}
return data;
}
auto write(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
advance(false);
if(address >= 0x1040 && address <= 0x107f) {
ram.write(address & 0x3f, data);
return data;
}
return data;
}
auto power(bool reset) -> void override {
arrangement = 0;
pendingArrangement = 0;
delay = 0;
pending = false;
ram.allocate(64, 0x00);
}
auto serialize(serializer& s) -> void override {
s(arrangement);
s(pendingArrangement);
s(delay);
s(pending);
s(ram);
}
private:
auto advance(bool isRead) -> void {
if(!pending) return;
if(delay) {
delay--;
return;
}
if(isRead) {
arrangement = pendingArrangement;
pending = false;
}
}
};
+39
View File
@@ -0,0 +1,39 @@
struct X07 : Interface {
using Interface::Interface;
Memory::Readable<n8> rom;
n4 bank;
auto load() -> void override {
Interface::load(rom, "program.rom");
}
auto read(n16 address, n8 data) -> n8 override {
address &= 0x1fff;
bankswitch(address);
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
return data;
}
auto write(n16 address, n8 data) -> n8 override {
bankswitch(address & 0x1fff);
return data;
}
auto power(bool reset) -> void override {
bank = 0;
}
auto serialize(serializer& s) -> void override {
s(bank);
}
private:
auto bankswitch(n16 address) -> void {
if((address & 0x180f) == 0x080d) {
bank = address.bit(4, 7);
} else if((address & 0x1880) == 0 && bank >= 14) {
bank = 14 | address.bit(6);
}
}
};
+88 -17
View File
@@ -18,24 +18,72 @@ auto Cartridge::connect() -> void {
information.title = pak->attribute("title");
information.region = pak->attribute("region");
information.board = pak->attribute("board");
information.phosphor = pak->attribute("phosphor").boolean();
if(auto fp = pak->read("program.rom")) {
information.sha256 = Hash::SHA256({fp->data(), fp->size()}).digest();
}
if(information.board == "Linear") board = std::make_unique<Board::Linear>(*this);
if(information.board == "Atari8k") board = std::make_unique<Board::Atari8k>(*this);
if(information.board == "Atari16k") board = std::make_unique<Board::Atari16k>(*this);
if(information.board == "Atari32k") board = std::make_unique<Board::Atari32k>(*this);
if(information.board == "Commavid") board = std::make_unique<Board::Commavid>(*this);
if(information.board == "ParkerBros8k") board = std::make_unique<Board::ParkerBros>(*this);
if(information.board == "Tigervision") board = std::make_unique<Board::Tigervision>(*this);
if(information.board == "Linear") board = std::make_unique<Board::Linear>(*this);
if(information.board == "Activision8k") board = std::make_unique<Board::Activision8k>(*this);
if(information.board == "Atari8k") board = std::make_unique<Board::Atari8k>(*this);
if(information.board == "Atari8kSC") board = std::make_unique<Board::Atari8k>(*this, true);
if(information.board == "Atari16k") board = std::make_unique<Board::Atari16k>(*this);
if(information.board == "Atari16kSC") board = std::make_unique<Board::Atari16k>(*this, true);
if(information.board == "Atari32k") board = std::make_unique<Board::Atari32k>(*this);
if(information.board == "Atari32kSC") board = std::make_unique<Board::Atari32k>(*this, true);
if(information.board == "Commavid") board = std::make_unique<Board::Commavid>(*this);
if(information.board == "ParkerBros8k") board = std::make_unique<Board::ParkerBros>(*this);
if(information.board == "ParkerBros03E0") board = std::make_unique<Board::ParkerBros03E0>(*this);
if(information.board == "JVP") board = std::make_unique<Board::JVP>(*this);
if(information.board == "Tigervision") board = std::make_unique<Board::Tigervision>(*this);
if(information.board == "UA8k") board = std::make_unique<Board::UA8k>(*this);
if(information.board == "UASW") board = std::make_unique<Board::UA8k>(*this, true);
if(information.board == "CbsRamPlus") board = std::make_unique<Board::CbsRamPlus>(*this);
if(information.board == "MNetwork") board = std::make_unique<Board::MNetwork>(*this);
if(information.board == "AmigaFC") board = std::make_unique<Board::AmigaFC>(*this);
if(information.board == "Wickstead") board = std::make_unique<Board::Wickstead>(*this);
if(information.board == "Jane") board = std::make_unique<Board::Jane>(*this);
if(information.board == "DPC") board = std::make_unique<Board::DPC>(*this);
if(information.board == "MegaBoy") board = std::make_unique<Board::MegaBoy>(*this);
if(information.board == "Atari32In1") board = std::make_unique<Board::Atari32In1>(*this);
//Homebrew
if(information.board == "DPC+") board = std::make_unique<Board::DPCPlus>(*this);
if(information.board == "CDF") board = std::make_unique<Board::CDF>(*this);
if(information.board == "BUS") board = std::make_unique<Board::BUS>(*this);
if(information.board == "Chetiry") board = std::make_unique<Board::Chetiry>(*this);
if(information.board == "FA2") board = std::make_unique<Board::FA2>(*this);
if(information.board == "ELF") board = std::make_unique<Board::ELFCartridge>(*this);
if(information.board == "MovieCart") board = std::make_unique<Board::MovieCart>(*this);
if(information.board == "4KSC") board = std::make_unique<Board::CPUWiz4KSC>(*this);
if(information.board == "3E") board = std::make_unique<Board::ThreeE>(*this);
if(information.board == "3EX") board = std::make_unique<Board::ThreeEX>(*this);
if(information.board == "3E+") board = std::make_unique<Board::ThreeEPlus>(*this);
if(information.board == "Enhanced3F") board = std::make_unique<Board::Enhanced3F>(*this);
if(information.board == "4A50") board = std::make_unique<Board::FourA50>(*this);
if(information.board == "EF") board = std::make_unique<Board::EF>(*this);
if(information.board == "EFF") board = std::make_unique<Board::EFF>(*this);
if(information.board == "DF") board = std::make_unique<Board::DF>(*this);
if(information.board == "BF") board = std::make_unique<Board::BF>(*this);
if(information.board == "EFSC") board = std::make_unique<Board::EFSC>(*this);
if(information.board == "DFSC") board = std::make_unique<Board::DFSC>(*this);
if(information.board == "BFSC") board = std::make_unique<Board::BFSC>(*this);
if(information.board == "MDM") board = std::make_unique<Board::MDM>(*this);
if(information.board == "X07") board = std::make_unique<Board::X07>(*this);
if(information.board == "EconoBanking") board = std::make_unique<Board::EconoBanking>(*this);
if(information.board == "Superbanking") board = std::make_unique<Board::Superbanking>(*this);
if(!board) board = std::make_unique<Board::Interface>(*this);
board->pak = pak;
board->load();
power();
video.setPhosphor(information.phosphor);
power(false);
}
auto Cartridge::disconnect() -> void {
if(!node || !board) return;
board->unload();
video.setPhosphor(false);
board->pak.reset();
board.reset();
node.reset();
@@ -46,21 +94,44 @@ auto Cartridge::save() -> void {
if(board) board->save();
}
auto Cartridge::power() -> void {
if(board) board->power();
auto Cartridge::power(bool reset) -> void {
if(board) board->power(reset);
}
auto Cartridge::read(n16 address) -> n8 {
if(!node) return 0xff;
if(board) return board->read(address);
return 0xff;
auto Cartridge::read(n16 address, n8 data) -> n8 {
if(!node) return data;
if(board) return board->read(address, data);
return data;
}
auto Cartridge::write(n16 address, n8 data) -> bool {
if(!node) return false;
auto Cartridge::write(n16 address, n8 data) -> n8 {
if(!node) return data;
if(board) return board->write(address, data);
return data;
}
auto Cartridge::armInvocation() const -> Harmony::Invocation {
if(board) return board->armInvocation();
return {};
}
auto Cartridge::readARM(u32 mode, n32 address, n32& data) -> Harmony::Access {
if(board) return board->readARM(mode, address, data);
return Harmony::Access::Unmapped;
}
auto Cartridge::writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access {
if(board) return board->writeARM(mode, address, data);
return Harmony::Access::Unmapped;
}
auto Cartridge::trapARM(u32 address, n32& value, n32 argument) -> bool {
if(board) return board->trapARM(address, value, argument);
return false;
}
auto Cartridge::stepARM(u32 clocks) -> void {
if(board) board->stepARM(clocks);
}
}
+12 -3
View File
@@ -7,6 +7,7 @@ struct Cartridge {
auto title() const -> string { return information.title; }
auto region() const -> string { return information.region; }
auto sha256() const -> string { return information.sha256; }
//cartridge.cpp
auto allocate(Node::Port) -> Node::Peripheral;
@@ -14,10 +15,16 @@ struct Cartridge {
auto disconnect() -> void;
auto save() -> void;
auto power() -> void;
auto power(bool reset) -> void;
auto read(n16 address) -> n8;
auto write(n16 address, n8 data) -> bool;
auto read(n16 address, n8 data) -> n8;
auto write(n16 address, n8 data) -> n8;
auto armInvocation() const -> Harmony::Invocation;
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access;
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access;
auto trapARM(u32 address, n32& value, n32 argument) -> bool;
auto stepARM(u32 clocks) -> void;
//serialization.cpp
auto serialize(serializer&) -> void;
@@ -29,6 +36,8 @@ struct Cartridge {
string title;
string region;
string board;
string sha256;
bool phosphor;
} information;
};
@@ -0,0 +1,9 @@
AmigaMouse::AmigaMouse(Node::Port parent)
: RelativePointingDevice(parent, "Amiga Mouse", 4) {
}
auto AmigaMouse::encode(u8 xPhase, u8 yPhase, bool, bool) const -> u8 {
static constexpr u8 horizontal[4] = {0b0000, 0b1000, 0b1010, 0b0010};
static constexpr u8 vertical[4] = {0b0000, 0b0100, 0b0101, 0b0001};
return horizontal[xPhase & 3] | vertical[yPhase & 3];
}
@@ -0,0 +1,6 @@
struct AmigaMouse : RelativePointingDevice {
AmigaMouse(Node::Port);
protected:
auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 override;
};
@@ -0,0 +1,9 @@
AtariMouse::AtariMouse(Node::Port parent)
: RelativePointingDevice(parent, "Atari Mouse", 4) {
}
auto AtariMouse::encode(u8 xPhase, u8 yPhase, bool, bool) const -> u8 {
static constexpr u8 horizontal[4] = {0b0000, 0b0001, 0b0011, 0b0010};
static constexpr u8 vertical[4] = {0b0000, 0b0100, 0b1100, 0b1000};
return horizontal[xPhase & 3] | vertical[yPhase & 3];
}
@@ -0,0 +1,6 @@
struct AtariMouse : RelativePointingDevice {
AtariMouse(Node::Port);
protected:
auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 override;
};
+92
View File
@@ -0,0 +1,92 @@
AtariVox::AtariVox(Node::Port parent) : SaveKey(parent, "AtariVox", "atarivox.eeprom") {
stream = node->append<Node::Audio::Stream>("AtariVox");
stream->setChannels(1);
stream->setFrequency(SpeakJet::Frequency);
speakjet.reset();
serialBits = 0;
serialData = 0;
serialValid = 1;
serialCycle = 0;
Thread::create(SpeakJet::Frequency, std::bind_front(&AtariVox::main, this));
}
AtariVox::~AtariVox() {
Thread::destroy();
stream.reset();
}
auto AtariVox::main() -> void {
stream->frame(speakjet.clock());
Thread::step(1);
Thread::synchronize(cpu);
}
auto AtariVox::power(bool reset) -> void {
SaveKey::power(reset);
speakjet.reset();
serialBits = 0;
serialData = 0;
serialValid = 1;
serialCycle = 0;
Thread::create(SpeakJet::Frequency, std::bind_front(&AtariVox::main, this));
}
auto AtariVox::read() -> n8 {
n8 data = SaveKey::read();
data.bit(1) = speakjet.ready();
return data;
}
auto AtariVox::write(n8 data) -> void {
SaveKey::write(data);
receiveSerial(data.bit(0));
}
auto AtariVox::serialize(serializer& s) -> void {
SaveKey::serialize(s);
Thread::serialize(s);
speakjet.serialize(s);
s(serialBits);
s(serialData);
s(serialValid);
s(serialCycle);
}
auto AtariVox::receiveSerial(bool level) -> void {
if(!cpu.scalar()) return;
//The CPU Thread advances three color clocks per 6507 cycle. Stella's serial
//thresholds are expressed in CPU cycles, so compare in that same domain.
auto cycle = cpu.clock() / cpu.scalar() / 3;
if(serialBits && (cycle < serialCycle || cycle > serialCycle + 1000)) {
serialBits = 0;
serialData = 0;
serialValid = 1;
}
if(!serialBits && level) {
serialCycle = cycle;
return;
}
if(serialBits && cycle >= serialCycle && cycle < serialCycle + 62) {
serialCycle = cycle;
return;
}
if(serialBits == 0) {
serialValid = !level;
} else if(serialBits <= 8) {
serialData.bit(serialBits - 1) = level;
} else {
serialValid &= level;
}
serialCycle = cycle;
if(++serialBits < 10) return;
if(serialValid) speakjet.enqueue(serialData);
serialBits = 0;
serialData = 0;
serialValid = 1;
}
+23
View File
@@ -0,0 +1,23 @@
#include "speakjet.hpp"
struct AtariVox : SaveKey, Thread {
AtariVox(Node::Port);
~AtariVox();
auto main() -> void;
auto power(bool reset) -> void override;
auto read() -> n8 override;
auto write(n8 data) -> void override;
auto serialize(serializer&) -> void override;
private:
auto receiveSerial(bool level) -> void;
Node::Audio::Stream stream;
SpeakJet speakjet;
n4 serialBits;
n8 serialData;
n1 serialValid;
u64 serialCycle;
};
+347
View File
@@ -0,0 +1,347 @@
namespace {
constexpr u16 AllophoneDuration[] = {
70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
70, 70, 70, 70, 70, 70, 70, 70, 70, 200, 200, 190,
200, 185, 165, 200, 225, 185, 170, 140, 180, 170, 170, 200,
131, 70, 70, 70, 70, 70, 45, 45, 10, 10, 45, 45,
10, 10, 55, 55, 55, 55, 70, 70, 70, 70, 70, 40,
40, 50, 40, 50, 70, 170, 55, 55, 55, 45, 99, 99,
};
constexpr u16 EffectDuration[] = {
80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
300, 101, 102, 540, 530, 500, 135, 600, 300, 250,
200, 270, 280, 260, 300, 100, 104, 100, 270, 262,
160, 300, 182, 120, 175, 350, 160, 260, 95, 75,
95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
125, 250, 530,
};
constexpr u16 PauseDuration[] = {0, 100, 200, 700, 30, 60, 90};
static_assert(std::size(AllophoneDuration) == 72);
static_assert(std::size(EffectDuration) == 55);
}
auto SpeakJet::reset() -> void {
std::fill(std::begin(fifo), std::end(fifo), 0);
fifoRead = 0;
fifoWrite = 0;
fifoCount = 0;
parameterPending = 0;
pendingCommand = 0;
waiting = 0;
volume = 96;
speed = 114;
pitch = 88;
bend = 5;
portControl = 7;
portOutput = 0;
repeatRequest = 0;
repeatCode = 0;
repeatRemaining = 0;
nextDurationScale = 1.0;
nextPitchScale = 1.0;
soundCode = 0;
soundClass = SoundClass::None;
soundSamples = 0;
soundRemaining = 0;
soundPitchScale = 1.0;
std::fill(std::begin(phase), std::end(phase), 0.0);
for(auto& stage : filter) std::fill(std::begin(stage), std::end(stage), 0.0);
noiseLfsr = 0x4a3d;
}
auto SpeakJet::enqueue(u8 data) -> bool {
if(fifoCount == 64) return false;
fifo[fifoWrite++] = data;
fifoCount++;
return true;
}
auto SpeakJet::clock() -> f64 {
if(!soundRemaining && !waiting) processCommands();
if(!soundRemaining) return 0.0;
auto sample = synthesize();
if(!--soundRemaining) finishSound();
return sample;
}
auto SpeakJet::serialize(serializer& s) -> void {
for(auto& byte : fifo) s(byte);
s(fifoRead);
s(fifoWrite);
s(fifoCount);
s(parameterPending);
s(pendingCommand);
s(waiting);
s(volume);
s(speed);
s(pitch);
s(bend);
s(portControl);
s(portOutput);
s(repeatRequest);
s(repeatCode);
s(repeatRemaining);
s(nextDurationScale);
s(nextPitchScale);
s(soundCode);
s(soundClass);
s(soundSamples);
s(soundRemaining);
s(soundPitchScale);
for(auto& value : phase) s(value);
for(auto& stage : filter) for(auto& value : stage) s(value);
s(noiseLfsr);
}
auto SpeakJet::dequeue() -> u8 {
if(!fifoCount) return 0;
auto data = fifo[fifoRead++];
fifoCount--;
return data;
}
auto SpeakJet::processCommands() -> void {
for(u32 commands : range(64)) {
if(parameterPending) {
if(!fifoCount) return;
parameter(pendingCommand, dequeue());
parameterPending = 0;
if(soundRemaining || waiting) return;
continue;
}
if(repeatRemaining) {
execute(repeatCode, true);
if(soundRemaining || waiting) return;
continue;
}
if(!fifoCount) return;
execute(dequeue());
if(soundRemaining || waiting) return;
}
}
auto SpeakJet::execute(u8 code, bool repeated) -> void {
if(repeated) repeatRemaining--;
if(code <= 6) {
startSound(code);
return;
}
switch(code) {
case 7: nextDurationScale = 0.5; return;
case 8: nextDurationScale = 1.5; return;
case 14: nextPitchScale = 1.08; return;
case 15: nextPitchScale = 0.92; return;
case 16: waiting = 1; return;
case 20: case 21: case 22: case 23: case 24: case 25: case 26: case 28: case 29: case 30:
pendingCommand = code;
parameterPending = 1;
return;
case 31:
volume = 96;
speed = 114;
pitch = 88;
bend = 5;
nextDurationScale = 1.0;
nextPitchScale = 1.0;
return;
case 255:
repeatRequest = 0;
repeatRemaining = 0;
nextDurationScale = 1.0;
nextPitchScale = 1.0;
return;
}
if(code < 128) return;
if(!repeated && repeatRequest) {
repeatCode = code;
repeatRemaining = repeatRequest;
repeatRequest = 0;
}
startSound(code);
}
auto SpeakJet::parameter(u8 command, u8 value) -> void {
switch(command) {
case 20: volume = min(value, (u8)127); break;
case 21: speed = min(value, (u8)127); break;
case 22: pitch = value; break;
case 23: bend = min(value, (u8)15); break;
case 24: portControl = value & 7; break;
case 25: portOutput = value & 7; break;
case 26: repeatRequest = value; break;
case 28: break; //Phrase EEPROM execution is outside the behavioral model.
case 29: break; //Phrase EEPROM execution is outside the behavioral model.
case 30:
soundCode = command;
soundClass = SoundClass::Pause;
auto samples = value * Frequency / 100.0;
soundSamples = soundRemaining = samples > 0.0 ? max(1u, (u32)(samples + 0.5)) : 0;
break;
}
}
auto SpeakJet::startSound(u8 code) -> void {
soundCode = code;
soundClass = classify(code);
auto milliseconds = duration(code);
auto scaled = milliseconds * Frequency / 1000.0;
scaled *= 114.0 / max(1.0, (f64)speed);
scaled *= nextDurationScale;
soundSamples = soundRemaining = scaled > 0.0 ? max(1u, (u32)(scaled + 0.5)) : 0;
soundPitchScale = nextPitchScale;
nextDurationScale = 1.0;
nextPitchScale = 1.0;
std::fill(std::begin(phase), std::end(phase), 0.0);
for(auto& stage : filter) std::fill(std::begin(stage), std::end(stage), 0.0);
}
auto SpeakJet::finishSound() -> void {
soundClass = SoundClass::None;
soundSamples = 0;
}
auto SpeakJet::synthesize() -> f64 {
if(soundClass == SoundClass::Pause) return 0.0;
auto progress = soundSamples ? 1.0 - (f64)soundRemaining / soundSamples : 1.0;
auto attack = min(1.0, progress * 12.0);
auto release = min(1.0, (1.0 - progress) * 10.0);
auto envelope = attack * release;
auto sample = soundCode < 200 ? synthesizeSpeech(progress) : synthesizeEffect(progress);
sample *= envelope * ((f64)volume / 127.0);
return max(-1.0, min(1.0, sample));
}
auto SpeakJet::synthesizeSpeech(f64 progress) -> f64 {
//The public MSA tables specify identities and durations, not oscillator trajectories.
//Use a deterministic phonetic-class model rather than importing a third-party voice table.
auto identity = soundCode - 128;
auto voiced = soundCode <= 181;
auto fundamental = (f64)pitch * soundPitchScale;
auto glottal = fundamental ? oscillator(0, fundamental) * 2.0 - 1.0 : 0.0;
auto source = voiced ? glottal : 0.0;
auto breath = noise();
if(soundClass == SoundClass::Fricative) source = voiced ? 0.4 * glottal + 0.75 * breath : breath;
if(soundClass == SoundClass::Affricate) source = 0.35 * glottal + 0.85 * breath;
if(soundClass == SoundClass::Stop) {
if(progress < 0.45) source = 0.0;
else source = (voiced ? 0.25 * glottal : 0.0) + breath * (1.0 - progress);
}
if(soundClass == SoundClass::Nasal) source = 0.75 * glottal + 0.1 * breath;
auto bendScale = 0.75 + (f64)bend * 0.05;
auto formant1 = (330.0 + (identity * 73) % 520) * bendScale;
auto formant2 = (850.0 + (identity * 97) % 1300) * bendScale;
auto formant3 = (2100.0 + (identity * 53) % 1050) * bendScale;
if(soundClass == SoundClass::Nasal) {
formant1 *= 0.72;
formant2 *= 0.82;
}
if(soundClass == SoundClass::Resonate) formant2 *= 0.88;
auto low = resonator(0, source, formant1, 110.0);
auto middle = resonator(1, source, formant2, 170.0);
auto high = resonator(2, source, formant3, 260.0);
return 0.95 * low + 0.65 * middle + 0.4 * high;
}
auto SpeakJet::synthesizeEffect(f64 progress) -> f64 {
auto index = soundCode % 10;
switch(soundClass) {
case SoundClass::Robot: {
auto carrier = oscillator(0, 90.0 + index * 31.0) < 0.5 ? -1.0 : 1.0;
auto modulator = oscillator(1, 8.0 + index * 2.0) * 2.0 - 1.0;
return carrier * (0.45 + 0.35 * modulator);
}
case SoundClass::Alarm: {
auto sweep = 250.0 + index * 45.0 + 900.0 * progress;
return 0.65 * sin(2.0 * Math::Pi * oscillator(0, sweep))
+ 0.25 * sin(2.0 * Math::Pi * oscillator(1, sweep * 1.51));
}
case SoundClass::Beep:
return 0.8 * sin(2.0 * Math::Pi * oscillator(0, 420.0 + index * 145.0));
case SoundClass::Biological: {
auto chirp = 130.0 + index * 37.0 + sin(progress * Math::Pi * 6.0) * (90.0 + index * 9.0);
return 0.6 * sin(2.0 * Math::Pi * oscillator(0, chirp)) + 0.2 * noise();
}
case SoundClass::DTMF: {
static constexpr u16 low[] = {941, 697, 697, 697, 770, 770, 770, 852, 852, 852, 941, 941};
static constexpr u16 high[] = {1336, 1209, 1336, 1477, 1209, 1336, 1477, 1209, 1336, 1477, 1209, 1477};
auto digit = soundCode - 240;
return 0.45 * sin(2.0 * Math::Pi * oscillator(0, low[digit]))
+ 0.45 * sin(2.0 * Math::Pi * oscillator(1, high[digit]));
}
case SoundClass::Sonar: {
auto frequency = 1500.0 - 900.0 * progress;
return sin(2.0 * Math::Pi * oscillator(0, frequency)) * (1.0 - progress);
}
case SoundClass::Pistol:
return noise() * (1.0 - progress) * (1.0 - progress);
case SoundClass::Wow: {
auto frequency = 180.0 + 780.0 * sin(progress * Math::Pi);
return 0.7 * sin(2.0 * Math::Pi * oscillator(0, frequency));
}
default: return 0.0;
}
}
auto SpeakJet::resonator(u32 index, f64 input, f64 frequency, f64 bandwidth) -> f64 {
frequency = min(frequency, 3999.0);
auto radius = exp(-Math::Pi * bandwidth / Frequency);
auto output = (1.0 - radius) * input
+ 2.0 * radius * cos(2.0 * Math::Pi * frequency / Frequency) * filter[index][0]
- radius * radius * filter[index][1];
filter[index][1] = filter[index][0];
filter[index][0] = output;
return output;
}
auto SpeakJet::oscillator(u32 index, f64 frequency) -> f64 {
phase[index] += min(frequency, 3999.0) / Frequency;
if(phase[index] >= 1.0) phase[index] -= 1.0;
return phase[index];
}
auto SpeakJet::noise() -> f64 {
auto feedback = (noiseLfsr ^ (noiseLfsr >> 2) ^ (noiseLfsr >> 3) ^ (noiseLfsr >> 5)) & 1;
noiseLfsr = (noiseLfsr >> 1) | feedback << 15;
return (noiseLfsr & 1) ? 1.0 : -1.0;
}
auto SpeakJet::duration(u8 code) -> u32 {
if(code <= 6) return PauseDuration[code];
if(code >= 128 && code <= 199) return AllophoneDuration[code - 128];
if(code >= 200 && code <= 254) return EffectDuration[code - 200];
return 0;
}
auto SpeakJet::classify(u8 code) -> SoundClass {
if(code <= 6) return SoundClass::Pause;
if(code >= 128 && (code <= 139 || code >= 149 && code <= 164)) return SoundClass::Vowel;
if(code >= 140 && code <= 144) return SoundClass::Nasal;
if(code >= 145 && code <= 148) return SoundClass::Resonate;
if(code == 165 || code == 182) return SoundClass::Affricate;
if((code >= 166 && code <= 169) || (code >= 183 && code <= 190)) return SoundClass::Fricative;
if((code >= 170 && code <= 181) || (code >= 191 && code <= 199)) return SoundClass::Stop;
if(code >= 200 && code <= 209) return SoundClass::Robot;
if(code >= 210 && code <= 219) return SoundClass::Alarm;
if(code >= 220 && code <= 229) return SoundClass::Beep;
if(code >= 230 && code <= 239) return SoundClass::Biological;
if(code >= 240 && code <= 251) return SoundClass::DTMF;
if(code == 252) return SoundClass::Sonar;
if(code == 253) return SoundClass::Pistol;
if(code == 254) return SoundClass::Wow;
return SoundClass::None;
}
+76
View File
@@ -0,0 +1,76 @@
struct SpeakJet {
static constexpr f64 Frequency = 8192.0;
enum class SoundClass : u8 {
None,
Vowel,
Nasal,
Resonate,
Affricate,
Fricative,
Stop,
Robot,
Alarm,
Beep,
Biological,
DTMF,
Sonar,
Pistol,
Wow,
Pause,
};
auto reset() -> void;
auto enqueue(u8 data) -> bool;
auto ready() const -> bool { return fifoCount < 64; }
auto buffered() const -> u32 { return fifoCount; }
auto speaking() const -> bool { return soundRemaining; }
auto clock() -> f64;
auto serialize(serializer&) -> void;
static auto duration(u8 code) -> u32;
static auto classify(u8 code) -> SoundClass;
private:
auto dequeue() -> u8;
auto processCommands() -> void;
auto execute(u8 code, bool repeated = false) -> void;
auto parameter(u8 command, u8 value) -> void;
auto startSound(u8 code) -> void;
auto finishSound() -> void;
auto synthesize() -> f64;
auto synthesizeSpeech(f64 progress) -> f64;
auto synthesizeEffect(f64 progress) -> f64;
auto resonator(u32 index, f64 input, f64 frequency, f64 bandwidth) -> f64;
auto oscillator(u32 index, f64 frequency) -> f64;
auto noise() -> f64;
u8 fifo[64] = {};
n6 fifoRead;
n6 fifoWrite;
n7 fifoCount;
n1 parameterPending;
u8 pendingCommand;
n1 waiting;
u8 volume;
u8 speed;
u8 pitch;
u8 bend;
n3 portControl;
n3 portOutput;
u8 repeatRequest;
u8 repeatCode;
u8 repeatRemaining;
f64 nextDurationScale;
f64 nextPitchScale;
u8 soundCode;
SoundClass soundClass;
u32 soundSamples;
u32 soundRemaining;
f64 soundPitchScale;
f64 phase[5] = {};
f64 filter[3][2] = {};
u16 noiseLfsr;
};
@@ -0,0 +1,10 @@
BoosterGrip::BoosterGrip(Node::Port parent) : Gamepad(parent, "Booster Grip") {
booster = node->append<Node::Input::Button>("Booster");
trigger = node->append<Node::Input::Button>("Trigger");
}
auto BoosterGrip::readAnalog(n1 index) -> AnalogConnection {
auto& button = index ? booster : trigger;
platform->input(button);
return button->value() ? AnalogConnection::vcc() : AnalogConnection::disconnected();
}
@@ -0,0 +1,8 @@
struct BoosterGrip : Gamepad {
Node::Input::Button booster;
Node::Input::Button trigger;
BoosterGrip(Node::Port);
auto readAnalog(n1 index) -> AnalogConnection override;
};
+19
View File
@@ -1,8 +1,27 @@
#include <a26/a26.hpp>
#include <ares/resource/resource.hpp>
namespace ares::Atari2600 {
#include "port.cpp"
#include "gamepad/gamepad.cpp"
#include "booster-grip/booster-grip.cpp"
#include "sega-genesis/sega-genesis.cpp"
#include "joy2b-plus/joy2b-plus.cpp"
#include "paddles/paddles.cpp"
#include "driving/driving.cpp"
#include "keyboard/keyboard.cpp"
#include "relative-pointing/relative-pointing.cpp"
#include "trak-ball/trak-ball.cpp"
#include "atari-mouse/atari-mouse.cpp"
#include "amiga-mouse/amiga-mouse.cpp"
#include "xg1-light-gun/xg1-light-gun.cpp"
#include "mindlink/mindlink.cpp"
#include "savekey/savekey.cpp"
#include "atarivox/speakjet.cpp"
#include "atarivox/atarivox.cpp"
#include "kidvid/audio.cpp"
#include "kidvid/kidvid.cpp"
#include "quadtari/quadtari.cpp"
}
+36
View File
@@ -1,11 +1,47 @@
struct Controller {
Node::Peripheral node;
struct AnalogConnection {
enum class Type : u32 { Disconnected, Ground, Vcc };
static auto disconnected() -> AnalogConnection { return {}; }
static auto ground(u32 resistance = 0) -> AnalogConnection { return {Type::Ground, resistance}; }
static auto vcc(u32 resistance = 0) -> AnalogConnection { return {Type::Vcc, resistance}; }
Type type = Type::Disconnected;
u32 resistance = 0;
};
virtual ~Controller() = default;
virtual auto save() -> void {}
virtual auto power(bool reset) -> void {}
virtual auto poll() -> void {}
virtual auto frame() -> void {}
virtual auto clock() -> void {}
virtual auto vblank(n1 dumped) -> void {}
virtual auto read() -> n8 { return 0xff; }
virtual auto write(n8 data) -> void {}
virtual auto controlWrite(n8 data) -> void {}
virtual auto readAnalog(n1 index) -> AnalogConnection { return AnalogConnection::disconnected(); }
virtual auto serialize(serializer&) -> void {}
};
#include "port.hpp"
#include "gamepad/gamepad.hpp"
#include "booster-grip/booster-grip.hpp"
#include "sega-genesis/sega-genesis.hpp"
#include "joy2b-plus/joy2b-plus.hpp"
#include "paddles/paddles.hpp"
#include "driving/driving.hpp"
#include "keyboard/keyboard.hpp"
#include "relative-pointing/relative-pointing.hpp"
#include "trak-ball/trak-ball.hpp"
#include "atari-mouse/atari-mouse.hpp"
#include "amiga-mouse/amiga-mouse.hpp"
#include "xg1-light-gun/xg1-light-gun.hpp"
#include "mindlink/mindlink.hpp"
#include "savekey/savekey.hpp"
#include "atarivox/atarivox.hpp"
#include "kidvid/kidvid.hpp"
#include "quadtari/quadtari.hpp"
+38
View File
@@ -0,0 +1,38 @@
Driving::Driving(Node::Port parent) {
node = parent->append<Node::Peripheral>("Driving");
wheel = node->append<Node::Input::Axis>("Wheel");
fire = node->append<Node::Input::Button>("Fire");
}
auto Driving::poll() -> void {
platform->input(wheel);
auto value = wheel->value();
s32 nextDirection = value < -16384 ? -1 : value > 16384 ? 1 : 0;
if(nextDirection && nextDirection != direction) {
auto phase = position.bit(8, 9);
if(nextDirection < 0) position = (phase + 4) * 256 - 1;
if(nextDirection > 0) position = (phase + 1) * 256;
} else if(nextDirection) {
auto distance = value / 512 + (value > 0);
position = (position + distance + 1024) & 1023;
}
direction = nextDirection;
}
auto Driving::read() -> n8 {
platform->input(fire);
static constexpr u8 gray[4] = {0x03, 0x01, 0x00, 0x02};
n8 data = 0xff;
data.bit(0, 1) = gray[position.bit(8, 9)];
data.bit(4) = !fire->value();
return data;
}
auto Driving::serialize(serializer& s) -> void {
s(position);
s(direction);
}
+14
View File
@@ -0,0 +1,14 @@
struct Driving : Controller {
Node::Input::Axis wheel;
Node::Input::Button fire;
Driving(Node::Port);
auto poll() -> void override;
auto read() -> n8 override;
auto serialize(serializer&) -> void override;
private:
n10 position = 0;
s8 direction = 0;
};
+6 -18
View File
@@ -1,5 +1,5 @@
Gamepad::Gamepad(Node::Port parent) {
node = parent->append<Node::Peripheral>("Gamepad");
Gamepad::Gamepad(Node::Port parent, string name) {
node = parent->append<Node::Peripheral>(name);
up = node->append<Node::Input::Button>("Up");
down = node->append<Node::Input::Button>("Down");
@@ -15,23 +15,11 @@ auto Gamepad::read() -> n8 {
platform->input(right);
platform->input(fire);
if(!(up->value() & down->value())) {
yHold = 0, upLatch = up->value(), downLatch = down->value();
} else if(!yHold) {
yHold = 1, swap(upLatch, downLatch);
}
if(!(left->value() & right->value())) {
xHold = 0, leftLatch = left->value(), rightLatch = right->value();
} else if(!xHold) {
xHold = 1, swap(leftLatch, rightLatch);
}
n8 data = 0xff;
data.bit(0) = !upLatch;
data.bit(1) = !downLatch;
data.bit(2) = !leftLatch;
data.bit(3) = !rightLatch;
data.bit(0) = !up->value();
data.bit(1) = !down->value();
data.bit(2) = !left->value();
data.bit(3) = !right->value();
data.bit(4) = !fire->value();
return data;
}
+1 -9
View File
@@ -5,15 +5,7 @@ struct Gamepad : Controller {
Node::Input::Button right;
Node::Input::Button fire;
Gamepad(Node::Port);
Gamepad(Node::Port, string name = "Gamepad");
auto read() -> n8 override;
private:
b1 yHold;
b1 upLatch;
b1 downLatch;
b1 xHold;
b1 leftLatch;
b1 rightLatch;
};
@@ -0,0 +1,10 @@
Joy2BPlus::Joy2BPlus(Node::Port parent) : Gamepad(parent, "Joy 2B+") {
buttonC = node->append<Node::Input::Button>("C");
button3 = node->append<Node::Input::Button>("Button 3");
}
auto Joy2BPlus::readAnalog(n1 index) -> AnalogConnection {
auto& button = index ? buttonC : button3;
platform->input(button);
return button->value() ? AnalogConnection::ground(330) : AnalogConnection::vcc();
}
@@ -0,0 +1,8 @@
struct Joy2BPlus : Gamepad {
Node::Input::Button buttonC;
Node::Input::Button button3;
Joy2BPlus(Node::Port);
auto readAnalog(n1 index) -> AnalogConnection override;
};
+39
View File
@@ -0,0 +1,39 @@
Keyboard::Keyboard(Node::Port parent) {
node = parent->append<Node::Peripheral>("Keyboard");
static constexpr const char* names[12] = {
"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"
};
for(u32 index : range(12)) key[index] = node->append<Node::Input::Button>(names[index]);
}
auto Keyboard::read() -> n8 {
n8 data = 0xff;
data.bit(4) = readColumn(2) != ColumnState::Ground;
return data;
}
auto Keyboard::write(n8 data) -> void {
rows = data.bit(0, 3);
}
auto Keyboard::readAnalog(n1 index) -> AnalogConnection {
switch(readColumn(index)) {
case ColumnState::Pullup: return AnalogConnection::vcc(4700);
case ColumnState::Ground: return AnalogConnection::ground();
case ColumnState::Vcc: return AnalogConnection::vcc();
}
unreachable;
}
auto Keyboard::readColumn(u32 column) -> ColumnState {
bool vcc = false;
for(u32 row : range(4)) {
auto& button = key[row * 3 + column];
platform->input(button);
if(!button->value()) continue;
if(!rows.bit(row)) return ColumnState::Ground;
vcc = true;
}
return vcc ? ColumnState::Vcc : ColumnState::Pullup;
}
+16
View File
@@ -0,0 +1,16 @@
struct Keyboard : Controller {
Node::Input::Button key[12];
Keyboard(Node::Port);
auto read() -> n8 override;
auto write(n8 data) -> void override;
auto readAnalog(n1 index) -> AnalogConnection override;
private:
enum class ColumnState : u32 { Pullup, Ground, Vcc };
auto readColumn(u32 column) -> ColumnState;
n4 rows = 0x0f;
};
+150
View File
@@ -0,0 +1,150 @@
auto KidVidAudio::load(VFS::Pak pak) -> void {
static constexpr u64 MaximumFileSize = 64_MiB;
static constexpr std::array<const char*, (u32)File::Count> Names = {
"KVS3.WAV", "KVS1.WAV", "KVS2.WAV",
"KVB3.WAV", "KVB1.WAV", "KVB2.WAV",
"KVSHARED.WAV",
};
reset();
sources = {};
mediaIdentity = 14695981039346656037ull;
for(u32 index : range((u32)File::Count)) {
auto fp = pak ? pak->read(Names[index]) : VFS::File{};
if(!fp) continue;
if(fp->size() > MaximumFileSize) continue;
auto& item = sources[index];
item.bytes.resize(fp->size());
if(!item.bytes.empty()) memory::copy(item.bytes.data(), fp->data(), item.bytes.size());
for(auto byte : item.bytes) {
mediaIdentity ^= byte;
mediaIdentity *= 1099511628211ull;
}
mediaIdentity ^= index;
mediaIdentity *= 1099511628211ull;
item.valid = parse(item);
}
}
auto KidVidAudio::reset() -> void {
activeFile = File::Shared;
cursor = 0;
end = 0;
active = false;
}
auto KidVidAudio::available(File file) const -> bool {
return file < File::Count && source(file).valid;
}
auto KidVidAudio::play(File file, u32 begin, u32 end) -> bool {
stop();
if(!available(file)) return false;
auto& item = source(file);
if(begin < item.dataBegin || end < begin || end > item.dataEnd) return false;
activeFile = file;
cursor = begin;
this->end = end;
active = cursor < end;
return active;
}
auto KidVidAudio::stop() -> void {
active = false;
cursor = end;
}
auto KidVidAudio::remaining() const -> u32 {
if(!active || cursor >= end) return 0;
return end - cursor;
}
auto KidVidAudio::clock() -> f64 {
if(!active) return 0.0;
auto& item = source(activeFile);
if(cursor >= end || cursor >= item.bytes.size()) {
stop();
return 0.0;
}
auto sample = item.bytes[cursor++];
if(cursor >= end) active = false;
return ((s32)sample - 128) / 128.0;
}
auto KidVidAudio::serialize(serializer& s) -> bool {
auto identity = mediaIdentity;
u32 file = (u32)activeFile;
s(identity);
s(file);
s(cursor);
s(end);
s(active);
if(s.reading()) {
if(identity != mediaIdentity || file >= (u32)File::Count) {
reset();
return false;
}
activeFile = (File)file;
if(!active) {
reset();
return true;
}
auto& item = source(activeFile);
if(!item.valid || cursor < item.dataBegin || end < cursor || end > item.dataEnd) {
reset();
return false;
}
}
return true;
}
auto KidVidAudio::parse(Source& source) -> bool {
auto& bytes = source.bytes;
if(bytes.size() < 12) return false;
auto tag = [&](u32 offset, const char* text) {
return offset <= bytes.size() - 4 && bytes[offset + 0] == text[0] && bytes[offset + 1] == text[1]
&& bytes[offset + 2] == text[2] && bytes[offset + 3] == text[3];
};
auto read16 = [&](u32 offset) -> u16 {
return (u16)bytes[offset] | (u16)bytes[offset + 1] << 8;
};
auto read32 = [&](u32 offset) -> u32 {
return (u32)bytes[offset] | (u32)bytes[offset + 1] << 8
| (u32)bytes[offset + 2] << 16 | (u32)bytes[offset + 3] << 24;
};
if(!tag(0, "RIFF") || !tag(8, "WAVE")) return false;
auto declaredEnd = (u64)read32(4) + 8;
if(declaredEnd > bytes.size() || declaredEnd < 12) return false;
bool formatValid = false;
bool dataFound = false;
for(u64 offset = 12; offset + 8 <= declaredEnd;) {
auto chunkSize = read32((u32)offset + 4);
auto dataBegin = offset + 8;
auto dataEnd = dataBegin + chunkSize;
if(dataEnd > declaredEnd || dataEnd < dataBegin) return false;
if(tag((u32)offset, "fmt ")) {
if(chunkSize < 16) return false;
formatValid = read16((u32)dataBegin + 0) == 1
&& read16((u32)dataBegin + 2) == 1
&& read32((u32)dataBegin + 4) == 16000
&& read16((u32)dataBegin + 12) == 1
&& read16((u32)dataBegin + 14) == 8;
}
if(tag((u32)offset, "data") && !dataFound) {
source.dataBegin = dataBegin;
source.dataEnd = dataEnd;
dataFound = true;
}
offset = dataEnd;
//Several canonical KidVid WAVs end on an odd-sized data chunk without a
//physical pad byte. A pad is required before another chunk, not after EOF.
if((chunkSize & 1) && offset < declaredEnd) offset++;
if(offset > declaredEnd) return false;
}
return formatValid && dataFound && source.dataBegin < source.dataEnd;
}
+41
View File
@@ -0,0 +1,41 @@
struct KidVidAudio {
enum class File : u32 {
Smurfs3,
Smurfs1,
Smurfs2,
Bears3,
Bears1,
Bears2,
Shared,
Count,
};
auto load(VFS::Pak) -> void;
auto reset() -> void;
auto available(File) const -> bool;
auto play(File, u32 begin, u32 end) -> bool;
auto stop() -> void;
auto playing() const -> bool { return active; }
auto remaining() const -> u32;
auto clock() -> f64;
auto serialize(serializer&) -> bool;
private:
struct Source {
std::vector<u8> bytes;
u32 dataBegin = 0;
u32 dataEnd = 0;
bool valid = false;
};
auto parse(Source&) -> bool;
auto source(File file) -> Source& { return sources[(u32)file]; }
auto source(File file) const -> const Source& { return sources[(u32)file]; }
std::array<Source, (u32)File::Count> sources;
u64 mediaIdentity = 0;
File activeFile = File::Shared;
u32 cursor = 0;
u32 end = 0;
bool active = false;
};
+216
View File
@@ -0,0 +1,216 @@
#include "tables.hpp"
static constexpr const char* KidVidSmurfsSha256 =
"bbe0f1d952aab334de82db18cd84feb49a7fd9beb60a39c52658483564af78be";
static constexpr const char* KidVidBearsSha256 =
"6161ac39cddfe7773c553f1b7a5925b703025ebe6ac18e934b8eb94465ed4548";
KidVid::KidVid(Node::Port parent) {
node = parent->append<Node::Peripheral>("KidVid Voice Module");
game1 = node->append<Node::Input::Button>("Game 1");
game2 = node->append<Node::Input::Button>("Game 2");
game3 = node->append<Node::Input::Button>("Game 3");
skip = node->append<Node::Input::Button>("Skip");
stream = node->append<Node::Audio::Stream>("KidVid");
stream->setChannels(1);
stream->setFrequency(16000);
node->setPak(pak = platform->pak(node));
audio.load(pak);
if(cartridge.sha256() == KidVidSmurfsSha256) profile = Profile::Smurfs;
if(cartridge.sha256() == KidVidBearsSha256) profile = Profile::Bears;
rewind();
Thread::create(16000, std::bind_front(&KidVid::main, this));
}
KidVid::~KidVid() {
Thread::destroy();
stream.reset();
pak.reset();
}
auto KidVid::main() -> void {
stream->frame(audio.clock());
Thread::step(1);
Thread::synchronize(cpu);
}
auto KidVid::power(bool reset) -> void {
if(!reset || profile == Profile::Smurfs) rewind();
Thread::create(16000, std::bind_front(&KidVid::main, this));
}
auto KidVid::poll() -> void {
platform->input(game1);
platform->input(game2);
platform->input(game3);
platform->input(skip);
}
auto KidVid::frame() -> void {
if(profile == Profile::Unknown) return;
if(profile == Profile::Smurfs && system.controls.reset->value()) rewind();
if(skip->value() && song && KidVidData::ClipOrder[song - 1] != 0
&& KidVidData::ClipOrder[song - 1] != 11) {
audio.stop();
}
if(!tape) {
if(game1->value()) select(1);
else if(game2->value()) select(2);
else if(game3->value()) select(3);
}
if(tape && run && !tapeBusy) advanceCassette();
updatePlayback();
}
auto KidVid::read() -> n8 {
n8 lines = 0xff;
lines.bit(3) = data;
return lines;
}
auto KidVid::write(n8 lines) -> void {
run = lines.bit(0);
}
auto KidVid::serialize(serializer& s) -> void {
Thread::serialize(s);
auto audioStateCompatible = audio.serialize(s);
s(tape);
s(bit);
s(bitsRemaining);
s(block);
s(song);
s(fallbackFrames);
s(run);
s(data);
s(tapeBusy);
s(songPlaying);
s(beep);
if(s.reading()) {
auto cassetteBits = KidVidData::CassetteData.size() * 8;
if(tape > 4 || bit >= cassetteBits || !bitsRemaining || bitsRemaining > 48
|| bitsRemaining > cassetteBits - bit || song > KidVidData::ClipOrder.size() || fallbackFrames > 92) {
rewind();
}
mediaAvailable = tape && audio.available(KidVidAudio::File::Shared) && audio.available(tapeFile());
if(!audioStateCompatible) {
mediaAvailable = false;
songPlaying = false;
beep = true;
tapeBusy = tape != 0;
fallbackFrames = tape ? 92 : 0;
}
}
}
auto KidVid::rewind() -> void {
audio.reset();
tape = 0;
bit = profile == Profile::Bears ? 48 : 0;
bitsRemaining = 48;
block = 0;
song = 0;
fallbackFrames = 0;
data = true;
tapeBusy = false;
songPlaying = false;
beep = false;
mediaAvailable = false;
}
auto KidVid::select(u32 game) -> void {
if(game < 1 || game > 3) return;
tape = game == 1 ? 2 : game == 2 ? 3 : profile == Profile::Bears ? 4 : 1;
bit = profile == Profile::Bears ? 48 : 0;
bitsRemaining = 48;
block = 0;
song = KidVidData::FirstClip[tapeIndex()];
fallbackFrames = 0;
tapeBusy = false;
songPlaying = false;
beep = false;
mediaAvailable = audio.available(KidVidAudio::File::Shared) && audio.available(tapeFile());
}
auto KidVid::tapeIndex() const -> u32 {
if(tape == 4) return 3;
return profile == Profile::Smurfs ? tape - 1 : tape + 2;
}
auto KidVid::tapeFile() const -> KidVidAudio::File {
return (KidVidAudio::File)tapeIndex();
}
auto KidVid::advanceCassette() -> void {
auto byte = KidVidData::CassetteData[bit >> 3];
data = byte >> (7 - (bit & 7)) & 1;
bit++;
if(--bitsRemaining) return;
if(block == 0) {
bit = 96 + (tape - 1) * 48;
} else if(block >= KidVidData::BlockCounts[tapeIndex()]) {
bit = 336;
} else {
bit = 288;
startNextClip();
}
block++;
bitsRemaining = 48;
}
auto KidVid::startNextClip() -> void {
if(!mediaAvailable || song >= KidVidData::ClipOrder.size()) {
beep = true;
tapeBusy = true;
songPlaying = false;
fallbackFrames = 92;
return;
}
auto entry = KidVidData::ClipOrder[song++];
beep = !(entry & 0x80);
auto clip = entry & 0x7f;
if(clip + 1 >= KidVidData::ClipBoundaries.size()) {
mediaAvailable = false;
return startNextClip();
}
auto begin = KidVidData::ClipBoundaries[clip];
auto end = KidVidData::ClipBoundaries[clip + 1];
if(end < begin + 786) {
mediaAvailable = false;
return startNextClip();
}
auto file = clip < 10 ? KidVidAudio::File::Shared : tapeFile();
if(!audio.play(file, begin, end - 786)) {
mediaAvailable = false;
return startNextClip();
}
songPlaying = true;
tapeBusy = true;
}
auto KidVid::updatePlayback() -> void {
if(mediaAvailable) {
if(!songPlaying) return;
auto remaining = audio.remaining();
tapeBusy = remaining > 262 * 60 || !beep;
if(remaining) return;
songPlaying = false;
tapeBusy = !beep;
if(!beep) startNextClip();
return;
}
if(fallbackFrames) {
fallbackFrames--;
tapeBusy = fallbackFrames > 60;
}
}
+47
View File
@@ -0,0 +1,47 @@
#include "audio.hpp"
struct KidVid : Controller, Thread {
KidVid(Node::Port);
~KidVid();
auto main() -> void;
auto power(bool reset) -> void override;
auto poll() -> void override;
auto frame() -> void override;
auto read() -> n8 override;
auto write(n8 data) -> void override;
auto serialize(serializer&) -> void override;
private:
enum class Profile : u32 { Unknown, Smurfs, Bears };
auto rewind() -> void;
auto select(u32 game) -> void;
auto tapeIndex() const -> u32;
auto tapeFile() const -> KidVidAudio::File;
auto advanceCassette() -> void;
auto startNextClip() -> void;
auto updatePlayback() -> void;
Node::Input::Button game1;
Node::Input::Button game2;
Node::Input::Button game3;
Node::Input::Button skip;
Node::Audio::Stream stream;
VFS::Pak pak;
KidVidAudio audio;
Profile profile = Profile::Unknown;
u32 tape = 0;
u32 bit = 0;
u32 bitsRemaining = 48;
u32 block = 0;
u32 song = 0;
u32 fallbackFrames = 0;
bool run = true;
bool data = true;
bool tapeBusy = false;
bool songPlaying = false;
bool beep = false;
bool mediaAvailable = false;
};
+75
View File
@@ -0,0 +1,75 @@
namespace KidVidData {
//Connector data recorded by the two supported programs. Each cassette block
//is six bytes and is shifted most-significant bit first.
static constexpr std::array<u8, 48> CassetteData = {
0x7b, 0x1e, 0xc6, 0x31, 0xec, 0x60,
0x7b, 0x1e, 0xc6, 0x3d, 0x8c, 0x60,
0xf6, 0x31, 0x8c, 0x63, 0x18, 0xc0,
0xf6, 0x31, 0x8c, 0x63, 0x18, 0xf0,
0xf6, 0x31, 0x8c, 0x63, 0x1e, 0xc0,
0xf6, 0x31, 0x8c, 0x63, 0x1e, 0xf0,
0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00,
0xf7, 0xb1, 0x8c, 0x63, 0x18, 0xc0,
};
//Order: Smurfs 3/1/2, then Bears 3/1/2.
static constexpr std::array<u8, 6> BlockCounts = {42, 23, 37, 102, 120, 102};
static constexpr std::array<u16, 6> FirstClip = {82, 0, 44, 266, 124, 186};
//The high bit marks a segment that ends without the cassette acknowledgement
//beep. The low seven bits select adjacent boundaries from ClipBoundaries.
static constexpr std::array<u8, 328> ClipOrder = {
11, 12+0x80, 13+0x80, 14, 15+0x80, 16, 8+0x80, 17, 18+0x80, 19, 20+0x80,
21, 8+0x80, 22, 15+0x80, 23, 18+0x80, 14, 20+0x80, 16, 18+0x80,
17, 15+0x80, 19, 8+0x80, 21, 20+0x80, 22, 18+0x80, 23, 15+0x80,
14, 20+0x80, 16, 8+0x80, 22, 15+0x80, 23, 18+0x80, 14, 20+0x80,
16, 8+0x80, 9,
25+0x80, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29,
30, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29,
30+0x80, 9,
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 34, 42, 36, 43, 40, 39, 38, 37,
34, 43, 36, 39, 40, 37, 38, 43, 34, 37, 36, 43, 40, 39, 38, 37, 34, 43,
36, 39, 40, 37, 38+0x80, 9,
0, 1, 45, 2, 3, 46, 4, 5, 47, 6, 7, 48, 4, 3, 49, 2, 1, 50, 6, 7, 51,
4, 5, 52, 6, 1, 53, 2, 7, 54, 6, 5, 45, 2, 1, 46, 4, 3, 47, 2, 5, 48,
4, 7, 49, 6, 1, 50, 2, 5, 51, 6, 3, 52, 4, 7, 53, 2, 1, 54, 6+0x80, 9,
0, 1, 56, 4, 3, 57, 2, 5, 58, 6, 7, 59, 2, 3, 60, 4, 1, 61, 6, 7, 62,
2, 5, 63, 6, 1, 64, 4, 7, 65, 6, 5, 66, 4, 1, 67, 2, 3, 68, 6, 5, 69,
2, 7, 70, 4, 1, 71, 2, 5, 72, 4, 3, 73, 6, 7, 74, 2, 1, 75, 6, 3, 76,
4, 5, 77, 6, 7, 78, 2, 3, 79, 4, 1, 80, 2, 7, 81, 4+0x80, 9,
0, 1, 83, 2, 3, 84, 4, 5, 85, 6, 7, 86, 4, 3, 87, 2, 1, 88, 6, 7, 89,
2, 5, 90, 6, 1, 91, 4, 7, 92, 6, 5, 93, 4, 1, 94, 2, 3, 95, 6, 5, 96,
2, 7, 97, 4, 1, 98, 6, 5, 99, 4, 3, 100, 2, 7, 101, 4, 1, 102, 2+0x80, 9,
};
//Absolute byte offsets in the canonical RIFF files. Indexes 0..10 belong to
//KVSHARED; every later index belongs to the selected tape file.
static constexpr std::array<u32, 104> ClipBoundaries = {
44, 980829, 1178398, 1430063, 1691136, 1841665, 2100386, 2283843, 2629588, 2824805, 3059116,
44, 164685, 395182, 750335, 962016, 1204273, 1538258, 1801683, 2086276, 2399093,
2589606, 2801287, 3111752, 3436329,
44, 778557, 1100782, 1293648, 1493569, 1801682, 2086275,
44, 686829, 893806, 1143119, 1385376, 1578241, 1942802, 2168595, 2493172,
2662517, 3022374, 3229351, 3720920,
44, 592749, 936142, 1465343, 1787568, 2145073, 2568434, 2822451, 3045892, 3709157, 4219542,
44, 303453, 703294, 1150175, 1514736, 2208577, 2511986, 2864787, 3306964, 3864389,
4148982, 4499431, 4824008, 5162697, 5581354, 5844779, 6162300, 6590365, 6839678,
7225407, 7552336, 7867505, 8316738, 8608387, 8940020, 9274005, 9593878,
44, 341085, 653902, 1018463, 1265424, 1669969, 1919282, 2227395, 2535508, 3057653,
3375174, 3704455, 4092536, 4487673, 5026282, 5416715, 5670732, 6225805, 6736190,
7110159, 7676992,
};
}
+47
View File
@@ -0,0 +1,47 @@
MindLink::MindLink(Node::Port parent) {
node = parent->append<Node::Peripheral>("MindLink");
x = node->append<Node::Input::Axis>("X");
trigger = node->append<Node::Input::Button>("Trigger");
}
auto MindLink::poll() -> void {
platform->input(x);
platform->input(trigger);
auto next = (s64)(position & ~TriggerValue) + x->value() * PositionScale;
position = std::clamp<s64>(next, MinimumPosition, MaximumPosition);
//Stella exposes this compatibility trigger, but the physical MindLink had no button.
if(trigger->value()) position |= TriggerValue;
lines = 0x1f;
shift = 1;
nextBit();
}
auto MindLink::read() -> n8 {
n8 data = 0xff;
data.bit(0, 4) = lines;
return data;
}
auto MindLink::write(n8 data) -> void {
lines.bit(0, 3) = data.bit(0, 3);
}
auto MindLink::controlWrite(n8 data) -> void {
if(data.bit(0)) nextBit();
}
auto MindLink::serialize(serializer& s) -> void {
s(position);
s(shift);
s(lines);
}
auto MindLink::nextBit() -> void {
lines.bit(2) = 0;
lines.bit(3) = (position & shift) != 0;
shift <<= 1;
}
+24
View File
@@ -0,0 +1,24 @@
struct MindLink : Controller {
Node::Input::Axis x;
Node::Input::Button trigger;
MindLink(Node::Port);
auto poll() -> void override;
auto read() -> n8 override;
auto write(n8 data) -> void override;
auto controlWrite(n8 data) -> void override;
auto serialize(serializer&) -> void override;
private:
auto nextBit() -> void;
u32 position = 0x2a00;
u32 shift = 1;
n5 lines = 0x1f;
static constexpr s32 PositionScale = 10;
static constexpr s32 MinimumPosition = 0x0b00;
static constexpr s32 MaximumPosition = 0x6500;
static constexpr u32 TriggerValue = 0x8000;
};
+28
View File
@@ -0,0 +1,28 @@
Paddles::Paddles(Node::Port parent, bool axes) {
node = parent->append<Node::Peripheral>("Paddles");
if(axes) {
axis[0] = node->append<Node::Input::Axis>("Paddle 1");
axis[1] = node->append<Node::Input::Axis>("Paddle 2");
}
fire[0] = node->append<Node::Input::Button>("Paddle 1 Fire");
fire[1] = node->append<Node::Input::Button>("Paddle 2 Fire");
}
auto Paddles::read() -> n8 {
platform->input(fire[0]);
platform->input(fire[1]);
n8 data = 0xff;
data.bit(3) = !fire[0]->value();
data.bit(2) = !fire[1]->value();
return data;
}
auto Paddles::readAnalog(n1 index) -> AnalogConnection {
if(!axis[index]) return AnalogConnection::disconnected();
platform->input(axis[index]);
auto value = std::clamp<s64>(axis[index]->value(), -32768, 32767);
auto resistance = (u64)(32767 - value) * MaximumResistance / 65535;
return AnalogConnection::vcc(resistance);
}
+11
View File
@@ -0,0 +1,11 @@
struct Paddles : Controller {
Node::Input::Axis axis[2];
Node::Input::Button fire[2];
Paddles(Node::Port, bool axes = true);
auto read() -> n8 override;
auto readAnalog(n1 index) -> AnalogConnection override;
static constexpr u32 MaximumResistance = 1'000'000;
};
+45 -4
View File
@@ -10,20 +10,61 @@ auto ControllerPort::load(Node::Object parent) -> void {
port->setType("Controller");
port->setHotSwappable(true);
port->setAllocate([&](auto name) { return allocate(name); });
port->setDisconnect([&] { device.reset(); });
port->setSupported({"Gamepad"});
port->setDisconnect([&] {
save();
device.reset();
});
std::vector<string> supported = {
"Gamepad", "Paddles", "Driving", "Keyboard",
"Booster Grip", "Sega Genesis", "Joy 2B+",
"CX-22 Trak-Ball", "CX-80 Trak-Ball", "Atari Mouse", "Amiga Mouse",
"XG-1 Light Gun", "MindLink", "SaveKey", "AtariVox", "QuadTari",
};
if(name == "Controller Port 2") supported.push_back("KidVid Voice Module");
port->setSupported(supported);
output = 0x0f;
}
auto ControllerPort::unload() -> void {
device = {};
port = {};
output = 0x0f;
}
auto ControllerPort::allocate(string name) -> Node::Peripheral {
if(name == "Gamepad") device = std::make_unique<Gamepad>(port);
if(device) return device->node;
auto role = this == &controllerPort2 ? Role::ConsoleRight : Role::ConsoleLeft;
device = create(port, name, role);
if(device) {
device->write(output);
return device->node;
}
return {};
}
auto ControllerPort::create(Node::Port port, string name, Role role) -> std::unique_ptr<Controller> {
auto quadTariChild = role == Role::QuadTariChild;
if(name == "Gamepad") return std::make_unique<Gamepad> (port);
if(name == "Paddles") return std::make_unique<Paddles> (port, !quadTariChild);
if(name == "Driving") return std::make_unique<Driving> (port);
if(name == "Keyboard") return std::make_unique<Keyboard> (port);
if(name == "Booster Grip") return std::make_unique<BoosterGrip>(port);
if(name == "Sega Genesis") return std::make_unique<SegaGenesis>(port);
if(name == "Joy 2B+") return std::make_unique<Joy2BPlus> (port);
if(name == "CX-22 Trak-Ball") return std::make_unique<TrakBall> (port, name);
if(name == "CX-80 Trak-Ball") return std::make_unique<TrakBall> (port, name);
if(name == "Atari Mouse") return std::make_unique<AtariMouse> (port);
if(name == "Amiga Mouse") return std::make_unique<AmigaMouse> (port);
if(name == "XG-1 Light Gun") return std::make_unique<XG1LightGun>(port);
if(name == "MindLink") return std::make_unique<MindLink> (port);
if(name == "SaveKey") return std::make_unique<SaveKey> (port);
if(name == "AtariVox") return std::make_unique<AtariVox> (port);
if(name == "KidVid Voice Module")
if(role == Role::ConsoleRight) return std::make_unique<KidVid> (port);
if(name == "QuadTari")
if(!quadTariChild) return std::make_unique<QuadTari> (port);
return {};
}
auto ControllerPort::serialize(serializer& s) -> void {
if(device) device->serialize(s);
}
+26 -1
View File
@@ -1,4 +1,6 @@
struct ControllerPort {
enum class Role : u32 { ConsoleLeft, ConsoleRight, QuadTariChild };
Node::Port port;
std::unique_ptr<Controller> device;
@@ -7,13 +9,36 @@ struct ControllerPort {
auto load(Node::Object) -> void;
auto unload() -> void;
auto allocate(string name) -> Node::Peripheral;
static auto create(Node::Port, string name, Role) -> std::unique_ptr<Controller>;
auto save() -> void { if(device) device->save(); }
auto power(bool reset) -> void {
if(device) {
device->power(reset);
device->write(output);
}
}
auto poll() -> void { if(device) device->poll(); }
auto frame() -> void { if(device) device->frame(); }
auto clock() -> void { if(device) device->clock(); }
auto vblank(n1 dumped) -> void { if(device) device->vblank(dumped); }
auto read() -> n8 { if(device) return device->read(); return 0xff; }
auto write(n8 data) -> void { if(device) return device->write(data); }
auto write(n8 data) -> void {
output = data;
if(device) device->write(data);
}
auto controlWrite(n4 data) -> void {
if(device) device->controlWrite(data);
}
auto readAnalog(n1 index) -> Controller::AnalogConnection {
if(device) return device->readAnalog(index);
return Controller::AnalogConnection::disconnected();
}
auto serialize(serializer&) -> void;
const string name;
n4 output = 0x0f;
};
extern ControllerPort controllerPort1;
+152
View File
@@ -0,0 +1,152 @@
QuadTari::QuadTari(Node::Port parent) : first(*this), second(*this) {
node = parent->append<Node::Peripheral>("QuadTari");
auto left = parent->name() == "Controller Port 1";
first.load(node, left ? "QuadTari Player 1" : "QuadTari Player 2");
second.load(node, left ? "QuadTari Player 3" : "QuadTari Player 4");
}
QuadTari::~QuadTari() {
first.unload();
second.unload();
}
auto QuadTari::Slot::load(Node::Peripheral parent, string name) -> void {
port = parent->append<Node::Port>(name);
port->setFamily(system.name());
port->setType("Controller");
port->setHotSwappable(true);
port->setAllocate([&](auto name) { return allocate(name); });
port->setDisconnect([&] {
save();
device.reset();
});
port->setSupported({"Gamepad", "Paddles", "Driving", "SaveKey", "AtariVox"});
port->allocate("Gamepad");
port->connect();
}
auto QuadTari::Slot::unload() -> void {
save();
device = {};
port = {};
}
auto QuadTari::Slot::allocate(string name) -> Node::Peripheral {
save();
device = {};
if(name != "Gamepad" && name != "Paddles" && name != "Driving" && name != "SaveKey" && name != "AtariVox") {
return {};
}
device = ControllerPort::create(port, name, ControllerPort::Role::QuadTariChild);
if(device) {
device->write(owner.output);
device->controlWrite(owner.control);
return device->node;
}
return {};
}
auto QuadTari::Slot::save() -> void {
if(device) device->save();
}
auto QuadTari::Slot::power(bool reset) -> void {
if(device) device->power(reset);
}
auto QuadTari::Slot::serialize(serializer& s) -> void {
if(device) device->serialize(s);
}
auto QuadTari::save() -> void {
first.save();
second.save();
}
auto QuadTari::power(bool reset) -> void {
first.power(reset);
second.power(reset);
dumped = 0;
selected = 0;
pending = 0;
settling = 0;
output = 0x0f;
control = 0x0f;
}
auto QuadTari::poll() -> void {
if(first.device) first.device->poll();
if(second.device) second.device->poll();
}
auto QuadTari::frame() -> void {
if(first.device) first.device->frame();
if(second.device) second.device->frame();
}
auto QuadTari::clock() -> void {
if(settling && !--settling) {
selected = pending;
synchronizeActive();
}
}
auto QuadTari::vblank(n1 dumped) -> void {
if(this->dumped == dumped) return;
this->dumped = dumped;
pending = dumped;
settling = SettlingClocks;
}
auto QuadTari::read() -> n8 {
if(auto device = active()) return device->read();
return 0xff;
}
auto QuadTari::write(n8 data) -> void {
output = data;
if(auto device = active()) device->write(data);
}
auto QuadTari::controlWrite(n8 data) -> void {
control = data;
if(auto device = active()) device->controlWrite(data);
}
auto QuadTari::readAnalog(n1 index) -> AnalogConnection {
return index ? AnalogConnection::vcc() : AnalogConnection::ground();
}
auto QuadTari::serialize(serializer& s) -> void {
s(dumped);
s(selected);
s(pending);
s(settling);
s(output);
s(control);
first.serialize(s);
second.serialize(s);
if(s.reading() && settling > SettlingClocks) {
dumped = 0;
selected = 0;
pending = 0;
settling = 0;
output = 0x0f;
control = 0x0f;
synchronizeActive();
}
}
auto QuadTari::active() -> Controller* {
auto& slot = selected ? second : first;
return slot.device.get();
}
auto QuadTari::synchronizeActive() -> void {
if(auto device = active()) {
device->write(output);
device->controlWrite(control);
}
}
+47
View File
@@ -0,0 +1,47 @@
struct QuadTari : Controller {
struct Slot {
Slot(QuadTari& owner) : owner(owner) {}
auto load(Node::Peripheral, string name) -> void;
auto unload() -> void;
auto allocate(string name) -> Node::Peripheral;
auto save() -> void;
auto power(bool reset) -> void;
auto serialize(serializer&) -> void;
QuadTari& owner;
Node::Port port;
std::unique_ptr<Controller> device;
};
QuadTari(Node::Port);
~QuadTari() override;
auto save() -> void override;
auto power(bool reset) -> void override;
auto poll() -> void override;
auto frame() -> void override;
auto clock() -> void override;
auto vblank(n1 dumped) -> void override;
auto read() -> n8 override;
auto write(n8 data) -> void override;
auto controlWrite(n8 data) -> void override;
auto readAnalog(n1 index) -> AnalogConnection override;
auto serialize(serializer&) -> void override;
static constexpr u16 SettlingClocks = 228;
private:
auto active() -> Controller*;
auto synchronizeActive() -> void;
public:
Slot first;
Slot second;
n1 dumped = 0;
n1 selected = 0;
n1 pending = 0;
u16 settling = 0;
n4 output = 0x0f;
n4 control = 0x0f;
};
@@ -0,0 +1,85 @@
RelativePointingDevice::RelativePointingDevice(Node::Port parent, string name, u32 scaleNumerator)
: scaleNumerator(scaleNumerator) {
node = parent->append<Node::Peripheral>(name);
x = node->append<Node::Input::Axis>("X");
y = node->append<Node::Input::Axis>("Y");
fire = node->append<Node::Input::Button>("Fire");
}
auto RelativePointingDevice::poll() -> void {
finish(horizontal);
finish(vertical);
platform->input(x);
platform->input(y);
queue(horizontal, -x->value());
queue(vertical, -y->value());
}
auto RelativePointingDevice::read() -> n8 {
advance(horizontal, cpu.clock());
advance(vertical, cpu.clock());
platform->input(fire);
n8 data = 0xff;
data.bit(0, 3) = encode(horizontal.phase, vertical.phase,
horizontal.direction < 0, vertical.direction > 0);
data.bit(4) = !fire->value();
return data;
}
auto RelativePointingDevice::finish(AxisState& axis) -> void {
if(axis.remaining) {
auto offset = axis.direction * (s32)(axis.remaining & 3);
axis.phase = (axis.phase + 4 + offset) & 3;
}
axis.remaining = 0;
axis.next = 0;
axis.interval = 1;
}
auto RelativePointingDevice::queue(AxisState& axis, s64 delta) -> void {
if(delta < -32768) delta = -32768;
if(delta > 32767) delta = 32767;
auto scaled = delta * scaleNumerator + axis.remainder;
auto transitions = scaled < 0
? (scaled - ScaleDenominator / 2) / ScaleDenominator
: (scaled + ScaleDenominator / 2) / ScaleDenominator;
axis.remainder = scaled - transitions * ScaleDenominator;
if(transitions < -(s64)MaximumTransitions) transitions = -(s64)MaximumTransitions;
if(transitions > (s64)MaximumTransitions) transitions = (s64)MaximumTransitions;
if(!transitions) return;
axis.direction = transitions < 0 ? -1 : 1;
axis.remaining = abs(transitions);
axis.interval = max((u64)1, windowDuration() / axis.remaining);
axis.next = axis.interval;
}
auto RelativePointingDevice::advance(AxisState& axis, u64 timestamp) -> void {
if(!axis.remaining || timestamp < axis.next) return;
auto due = 1 + (timestamp - axis.next) / axis.interval;
if(due > axis.remaining) due = axis.remaining;
auto offset = axis.direction * (s32)(due & 3);
axis.phase = (axis.phase + 4 + offset) & 3;
axis.remaining -= due;
axis.next += axis.interval * due;
}
auto RelativePointingDevice::windowDuration() const -> u64 {
return cpu.scalar() * 228 * (Region::NTSC() ? 262 : 312);
}
auto RelativePointingDevice::serialize(serializer& s) -> void {
for(auto* axis : {&horizontal, &vertical}) {
s(axis->phase);
s(axis->direction);
s(axis->remaining);
s(axis->next);
s(axis->interval);
s(axis->remainder);
}
}
@@ -0,0 +1,33 @@
struct RelativePointingDevice : Controller {
Node::Input::Axis x;
Node::Input::Axis y;
Node::Input::Button fire;
RelativePointingDevice(Node::Port, string name, u32 scaleNumerator);
auto poll() -> void override;
auto read() -> n8 override;
auto serialize(serializer&) -> void override;
protected:
virtual auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 = 0;
private:
struct AxisState {
u8 phase = 0;
s8 direction = 0;
u32 remaining = 0;
u64 next = 0;
u64 interval = 1;
s32 remainder = 0;
} horizontal, vertical;
auto finish(AxisState&) -> void;
auto queue(AxisState&, s64 delta) -> void;
auto advance(AxisState&, u64 timestamp) -> void;
auto windowDuration() const -> u64;
const u32 scaleNumerator;
static constexpr u32 ScaleDenominator = 5;
static constexpr u32 MaximumTransitions = 32767;
};
+48
View File
@@ -0,0 +1,48 @@
SaveKey::SaveKey(Node::Port parent, string name, string filename) {
node = parent->append<Node::Peripheral>(name);
node->setPak(pak = platform->pak(node));
persistent.load(pak, filename, 32_KiB, 0xff);
eeprom.load(M24C::Type::M24C256);
synchronizeEeprom();
eeprom.power();
}
auto SaveKey::save() -> void {
synchronizePersistent();
persistent.flush(pak);
}
auto SaveKey::power(bool reset) -> void {
eeprom.power();
}
auto SaveKey::read() -> n8 {
n8 data = 0xff;
data.bit(2) = eeprom.read();
return data;
}
auto SaveKey::write(n8 data) -> void {
auto transactionBoundary = eeprom.clock() && data.bit(3) && eeprom.data() != data.bit(2);
eeprom.clock = eeprom.clock();
eeprom.data = eeprom.data();
eeprom.clock = data.bit(3);
eeprom.data = data.bit(2);
eeprom.write();
if(transactionBoundary) synchronizePersistent();
}
auto SaveKey::serialize(serializer& s) -> void {
if(s.writing()) synchronizePersistent();
eeprom.serialize(s, false);
if(s.reading()) synchronizeEeprom();
}
auto SaveKey::synchronizeEeprom() -> void {
std::copy(persistent.memory.begin(), persistent.memory.end(), std::begin(eeprom.memory));
}
auto SaveKey::synchronizePersistent() -> void {
persistent.replace({(const u8*)eeprom.memory, eeprom.size()});
}
+17
View File
@@ -0,0 +1,17 @@
struct SaveKey : Controller {
SaveKey(Node::Port, string name = "SaveKey", string filename = "savekey.eeprom");
auto save() -> void override;
auto power(bool reset) -> void override;
auto read() -> n8 override;
auto write(n8 data) -> void override;
auto serialize(serializer&) -> void override;
protected:
auto synchronizeEeprom() -> void;
auto synchronizePersistent() -> void;
VFS::Pak pak;
M24C eeprom;
PersistentMemory persistent;
};
@@ -0,0 +1,9 @@
SegaGenesis::SegaGenesis(Node::Port parent) : Gamepad(parent, "Sega Genesis") {
buttonC = node->append<Node::Input::Button>("C");
}
auto SegaGenesis::readAnalog(n1 index) -> AnalogConnection {
if(!index) return AnalogConnection::vcc();
platform->input(buttonC);
return buttonC->value() ? AnalogConnection::ground() : AnalogConnection::vcc();
}
@@ -0,0 +1,7 @@
struct SegaGenesis : Gamepad {
Node::Input::Button buttonC;
SegaGenesis(Node::Port);
auto readAnalog(n1 index) -> AnalogConnection override;
};
@@ -0,0 +1,13 @@
TrakBall::TrakBall(Node::Port parent, string name)
: RelativePointingDevice(parent, name, 2) {
}
auto TrakBall::encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 {
static constexpr u8 horizontal[2][2] = {
{0b00, 0b01}, {0b10, 0b11},
};
static constexpr u8 vertical[2][2] = {
{0b0100, 0b0000}, {0b1100, 0b1000},
};
return horizontal[xPhase & 1][left] | vertical[yPhase & 1][down];
}
@@ -0,0 +1,6 @@
struct TrakBall : RelativePointingDevice {
TrakBall(Node::Port, string name);
protected:
auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 override;
};
@@ -0,0 +1,56 @@
XG1LightGun::XG1LightGun(Node::Port parent) {
node = parent->append<Node::Peripheral>("XG-1 Light Gun");
x = node->append<Node::Input::Axis>("X");
y = node->append<Node::Input::Axis>("Y");
trigger = node->append<Node::Input::Button>("Trigger");
targetY = video.displayHeight() / 2;
sprite = node->append<Node::Video::Sprite>("Crosshair");
sprite->setImage(Resource::Sprite::Famicom::Crosshair);
video.screen->attach(sprite);
updateCrosshair();
}
XG1LightGun::~XG1LightGun() {
if(video.screen) video.screen->detach(sprite);
}
auto XG1LightGun::poll() -> void {
platform->input(x);
platform->input(y);
targetX = std::clamp<s64>(targetX + x->value(), -TargetMargin, 160 + TargetMargin - 1);
targetY = std::clamp<s64>(targetY + y->value(), -TargetMargin,
video.displayHeight() + TargetMargin - 1);
updateCrosshair();
}
auto XG1LightGun::read() -> n8 {
platform->input(trigger);
n8 data = 0xff;
data.bit(0) = !trigger->value();
data.bit(4) = !lightDetected();
return data;
}
auto XG1LightGun::serialize(serializer& s) -> void {
s(targetX);
s(targetY);
if(s.reading()) updateCrosshair();
}
auto XG1LightGun::lightDetected() const -> bool {
auto beamX = tia.displayPosition() - 68 + HorizontalOffset;
if(beamX < 0) beamX += HorizontalClocks;
auto beamY = video.y() + VerticalOffset;
auto deltaX = beamX - targetX;
auto deltaY = beamY - targetY;
return deltaX >= 0 && deltaX < DetectionWidth && deltaY >= 0;
}
auto XG1LightGun::updateCrosshair() -> void {
sprite->setPosition(targetX + VideoLeftMargin - CrosshairRadius, targetY - CrosshairRadius);
sprite->setVisible(targetX >= 0 && targetX < 160 && targetY >= 0 && targetY < video.displayHeight());
}
@@ -0,0 +1,28 @@
struct XG1LightGun : Controller {
Node::Input::Axis x;
Node::Input::Axis y;
Node::Input::Button trigger;
Node::Video::Sprite sprite;
XG1LightGun(Node::Port);
~XG1LightGun();
auto poll() -> void override;
auto read() -> n8 override;
auto serialize(serializer&) -> void override;
private:
auto lightDetected() const -> bool;
auto updateCrosshair() -> void;
i16 targetX = 80;
i16 targetY = 114;
static constexpr s32 TargetMargin = 16;
static constexpr s32 HorizontalClocks = 228;
static constexpr s32 HorizontalOffset = -23;
static constexpr s32 VerticalOffset = 1;
static constexpr s32 DetectionWidth = 15;
static constexpr s32 CrosshairRadius = 8;
static constexpr s32 VideoLeftMargin = 10;
};
+10 -3
View File
@@ -19,22 +19,29 @@ auto CPU::unload() -> void {
}
auto CPU::main() -> void {
if(io.resetPending) {
debugger.interrupt("Reset");
reset();
io.resetPending = 0;
}
debugger.instruction();
instruction();
}
auto CPU::step(u32 clocks) -> void {
if(io.rdyLine == 1) io.scanlineCycles += clocks;
Thread::step(clocks);
Thread::step(clocks * 3);
Thread::synchronize();
}
auto CPU::power(bool reset) -> void {
MOS6502::BCD = 1;
MOS6502::power();
Thread::create(system.frequency() / 3, std::bind_front(&CPU::main, this));
if(!reset) MOS6502::power();
Thread::create(system.frequency(), std::bind_front(&CPU::main, this));
io = {};
io.resetPending = 1;
}
}
Loaded 100 of 1971 files, more files were not shown because too many files have changed in this diff. Show more