diff --git a/.clang-format b/.clang-format
new file mode 100644
index 00000000..d7de8bec
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,34 @@
+# clang-format config for virtualjaguar-libretro.
+#
+# DELIBERATELY MINIMAL. The upstream codebase has mixed brace style
+# AND mixed indent (tabs in src/tom/*, spaces in libretro.c, Allman
+# braces in src/jerry/jerry.h, K&R elsewhere) per-file. No single
+# style works. This config only enforces things that are universally
+# consistent across the tree: don't sort includes, don't reflow long
+# lines, don't touch preprocessor indent. Brace placement, spacing,
+# and indent style are NOT enforced.
+#
+# The CI check (`clang-format.yml`) runs clang-format-diff on changed
+# lines and posts suggestions to the job summary -- it does NOT fail
+# the build. Use it as a "consider this" prompt, not a gate.
+
+BasedOnStyle: LLVM
+Language: Cpp
+
+# Don't reflow long lines -- author makes wrapping decisions.
+ColumnLimit: 0
+
+# libretro / UAE include order is hand-tuned; don't reorder.
+SortIncludes: false
+IncludeBlocks: Preserve
+
+# Don't touch preprocessor indent -- breaks ifdef-guarded code.
+IndentPPDirectives: None
+
+# Tolerate both tab and space indent without rewriting.
+UseTab: ForIndentation
+TabWidth: 4
+IndentWidth: 4
+
+# Don't reformat comments (preserves the upstream's ASCII art).
+ReflowComments: false
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 00000000..dffc5c4f
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,61 @@
+# clang-tidy config for virtualjaguar-libretro.
+#
+# Curated check list aimed at catching real bugs in emulator C code
+# without flooding the diff with style-preference noise. See the
+# CI workflow `clang-tidy` job for how this is invoked (only on
+# PR-changed files).
+#
+# Disabled checks rationale below each `-` entry.
+
+Checks: >
+ bugprone-*,
+ clang-analyzer-*,
+ readability-inconsistent-declaration-parameter-name,
+ readability-misleading-indentation,
+ readability-redundant-control-flow,
+ misc-redundant-expression,
+ -bugprone-easily-swappable-parameters,
+ -bugprone-narrowing-conversions,
+ -bugprone-implicit-widening-of-multiplication-result,
+ -bugprone-reserved-identifier,
+ -bugprone-assignment-in-if-condition,
+ -bugprone-macro-parentheses,
+ -bugprone-signed-char-misuse,
+ -bugprone-suspicious-include,
+ -bugprone-switch-missing-default-case,
+ -bugprone-branch-clone,
+ -clang-analyzer-deadcode.DeadStores,
+ -clang-analyzer-optin.portability.UnixAPI,
+ -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
+ -clang-analyzer-valist.Uninitialized
+
+# Don't promote to errors yet -- the workflow itself decides via
+# --warnings-as-errors / continue-on-error.
+WarningsAsErrors: ''
+
+# Apply checks to in-tree headers but skip vendored / generated.
+HeaderFilterRegex: '^(src/(core|tom|jerry|cd)|libretro\.c).*'
+
+# Don't run clang-format inline (we have a separate clang-format job).
+FormatStyle: none
+
+# Disabled-check rationale:
+# - easily-swappable-parameters: emulator hot paths take many same-typed args.
+# - narrowing-conversions / implicit-widening: register byte ops trip these constantly.
+# - reserved-identifier: __LIBRETRO__ and UAE __regs/__pads use reserved names by design.
+# - assignment-in-if-condition: idiomatic in dispatch loops.
+# - macro-parentheses: misfires on GET16/SET32 byte-swap macros.
+# - signed-char-misuse: ROM byte buffers commonly use signed char.
+# - suspicious-include: project includes .c files in a couple of dispatch headers.
+# - switch-missing-default-case: cosmetic; switches on bit-field decode patterns
+# commonly omit default because all valid bit values are handled.
+# - branch-clone: register-decode if-chains in src/cd/cdrom.c and src/tom/tom.c
+# intentionally write the same value for several adjacent register addresses
+# to make the address->effect mapping legible. Real bug clones are caught
+# by code review, not this check.
+# - deadcode.DeadStores: blitter / OP / register-decode functions self-doc-init
+# locals that are read via macros (BCOMPEN, DSTA2, ...) clang-tidy can't see
+# the linkage for. Removing these inits would introduce real bugs.
+# - DeprecatedOrUnsafeBufferHandling: MSVC-flavored noise about strcpy/sprintf.
+# - optin.portability.UnixAPI: false positive on libretro_core_options.h calloc.
+# - valist.Uninitialized: false-positive prone on our log macros.
diff --git a/.ecrc b/.ecrc
new file mode 100644
index 00000000..b73e077e
--- /dev/null
+++ b/.ecrc
@@ -0,0 +1,17 @@
+{
+ "Exclude": [
+ "test/roms/.*",
+ "test/baselines/.*",
+ "test/tools/build/.*",
+ "build/.*",
+ "src/m68000/.*",
+ "src/bios/jag.*\\.c$",
+ "src/core/version.h",
+ "libretro-common/.*",
+ "docs/atari-jaguar-1999/.*",
+ ".*\\.(png|jpg|jpeg|gif|bmp|ico|pdf|j64|jag|rom|cof|abs|bin|chd|cue|iso|cdi|so|dll|dylib|a|o|bc|exe|dat|gz|tar|zip|tgz|7z)$"
+ ],
+ "Disable": {
+ "MaxLineLength": true
+ }
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..c05eab14
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,56 @@
+# EditorConfig: https://editorconfig.org
+#
+# Project-wide consistency for line endings / charset / final newline.
+# Indentation style is intentionally NOT enforced for C sources here:
+# the upstream Virtual Jaguar tree mixes tabs (src/tom, src/jerry) with
+# 4-space (libretro.c, src/core/cheat.c) and a sweeping reformat is out
+# of scope. Makefiles (tabs required) and YAML (2-space) are pinned.
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# Makefiles: recipe lines must be tabs (make will fail loudly if not),
+# but ifeq/else if blocks in this tree mix tab/space indentation for
+# readability; we don't enforce a single style here.
+
+# GitHub Actions / generic YAML.
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+
+# Shell scripts: only enforce no-tabs; the existing tree uses 3-space
+# in some files and 2-space in others, both fine.
+[*.sh]
+indent_style = space
+
+# Markdown: trailing two spaces are a hard line-break — don't strip.
+[*.md]
+trim_trailing_whitespace = false
+
+# Machine-generated UAE 68K core: do not touch.
+[src/m68000/**]
+charset = unset
+end_of_line = unset
+insert_final_newline = unset
+trim_trailing_whitespace = unset
+indent_style = unset
+indent_size = unset
+
+# Embedded BIOS / boot stubs are bin2c-generated hex arrays;
+# every line ends in trailing whitespace by design.
+[src/bios/jag*.c]
+trim_trailing_whitespace = unset
+
+# libretro-common is a vendored subtree.
+[libretro-common/**]
+charset = unset
+end_of_line = unset
+insert_final_newline = unset
+trim_trailing_whitespace = unset
+indent_style = unset
+indent_size = unset
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 121e7837..8d015de5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @JoeMatt @twinaphex
+* @JoeMatt @twinaphex
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..3b3f5f59
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,98 @@
+name: Bug report
+description: A reproducible behavior bug (crash, wrong output, hang, freeze) in a specific game or scenario.
+title: "[bug] "
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for filing. The more reproducible this is, the faster it gets fixed. For perf regressions, use the `Performance issue` template instead.
+
+ - type: input
+ id: game
+ attributes:
+ label: Game / ROM
+ description: Title, region, dump notes (homebrew vs commercial, .jag vs .j64, CD vs cart).
+ placeholder: "e.g. Atari Karts (1995, USA, .jag)"
+ validations:
+ required: true
+
+ - type: textarea
+ id: repro
+ attributes:
+ label: Steps to reproduce
+ description: What you did, in order, including which menu screens / inputs.
+ placeholder: |
+ 1. Boot ROM
+ 2. Press Start at title
+ 3. Select Single Race
+ 4. Crash on track-select screen
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected vs actual
+ placeholder: |
+ Expected: track-select renders normally.
+ Actual: black screen, audio loops 0.5s buffer, RetroArch logs "GPU stalled".
+ validations:
+ required: true
+
+ - type: dropdown
+ id: bios
+ attributes:
+ label: BIOS mode
+ description: Set in RetroArch core options as `virtualjaguar_bios`.
+ options:
+ - "HLE BIOS (default; no real BIOS file)"
+ - "Real BIOS (jagboot.rom in system/)"
+ - "Not sure"
+ validations:
+ required: true
+
+ - type: dropdown
+ id: blitter
+ attributes:
+ label: Blitter mode
+ description: Set in RetroArch core options as `virtualjaguar_usefastblitter`.
+ options:
+ - "Fast (default)"
+ - "Accurate (Midsummer2)"
+ - "Both (issue is independent of blitter)"
+ validations:
+ required: true
+
+ - type: input
+ id: core_version
+ attributes:
+ label: Core version
+ description: RetroArch -> Information -> Core Information -> "Core Version", or strings on the binary. (e.g. v2.2.0 abc1234)
+ validations:
+ required: true
+
+ - type: input
+ id: frontend
+ attributes:
+ label: Frontend + platform
+ placeholder: "e.g. RetroArch 1.21.0, macOS 14.5 (arm64); or Provenance 3.x iOS"
+ validations:
+ required: true
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs
+ description: Paste relevant lines from the RetroArch log (Settings -> Logging -> Logging Verbosity = Debug, then check `~/Library/Application Support/RetroArch/logs/` on macOS or platform equivalent). Use `Log
... ` for long pastes.
+ render: shell
+
+ - type: checkboxes
+ id: pre
+ attributes:
+ label: Pre-flight
+ options:
+ - label: Verified the issue is in this libretro core (not standalone Virtual Jaguar or another emulator).
+ required: true
+ - label: Searched [existing issues](https://github.com/libretro/virtualjaguar-libretro/issues?q=is%3Aissue) for duplicates.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..a24ddca6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: libretro Discord
+ url: https://discord.com/invite/27Xxm2h
+ about: General libretro / RetroArch questions are best in the Discord; this tracker is for Virtual Jaguar core bugs and feature work.
+ - name: Upstream Virtual Jaguar
+ url: http://shamusworld.gotdns.org/git/virtualjaguar
+ about: Issues that are reproducible in standalone Virtual Jaguar (not specific to the libretro core) belong upstream.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..bfa27682
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,39 @@
+name: Feature request
+description: A new capability you'd like the core to expose.
+title: "[feat] "
+labels: ["enhancement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before filing: this is a libretro fork of upstream Virtual Jaguar. Hardware-emulation requests should usually go upstream; this tracker is for libretro-specific surface (core options, save state shape, RetroAchievements, input remapping, etc.).
+
+ - type: textarea
+ id: what
+ attributes:
+ label: What
+ description: One sentence on what you want.
+ validations:
+ required: true
+
+ - type: textarea
+ id: why
+ attributes:
+ label: Why / use case
+ description: Concrete scenario. "I want X so I can do Y" beats "X would be nice".
+ validations:
+ required: true
+
+ - type: textarea
+ id: how
+ attributes:
+ label: How (if you have ideas)
+ description: Optional. Pointers to where you think it'd hook in (`libretro_core_options.h`, a specific subsystem) help if you have them.
+
+ - type: checkboxes
+ id: pre
+ attributes:
+ label: Pre-flight
+ options:
+ - label: This is libretro-fork-specific, not a general Virtual Jaguar emulation feature (those go upstream).
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/performance_issue.yml b/.github/ISSUE_TEMPLATE/performance_issue.yml
new file mode 100644
index 00000000..cbcce0d1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/performance_issue.yml
@@ -0,0 +1,77 @@
+name: Performance issue
+description: Slowdown, frame drops, or audio stutter in a specific game or scenario.
+title: "[perf] "
+labels: ["performance"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Use this for perf regressions, frame drops, audio stutter, slow boot. For functional bugs (crashes, wrong output), use the `Bug report` template instead.
+
+ Run `make benchmark BENCH_ROM=` from a fresh clone to get a stable wall-clock number that's easier to triage than "it feels slow". See [`docs/profiling.md`](https://github.com/libretro/virtualjaguar-libretro/blob/develop/docs/profiling.md).
+
+ - type: input
+ id: game
+ attributes:
+ label: Game / ROM
+ placeholder: "e.g. Wolfenstein 3D (1994, USA, .jag)"
+ validations:
+ required: true
+
+ - type: textarea
+ id: scenario
+ attributes:
+ label: Scenario
+ description: Where in the game. "Title screen" vs "level 3 with 5 enemies on screen" matter.
+ placeholder: "Level 3 first room: 4 visible enemies + animated wall textures."
+ validations:
+ required: true
+
+ - type: input
+ id: bench
+ attributes:
+ label: Benchmark numbers (if you have them)
+ description: Output of `make benchmark BENCH_ROM=` -- frames/sec, ms/frame. Mention CPU + OS so we can sanity-check.
+ placeholder: "make benchmark BENCH_ROM=Wolf3D.jag -> 42 FPS / 23.8 ms (M2 MBP, macOS 14.5)"
+
+ - type: dropdown
+ id: blitter
+ attributes:
+ label: Blitter mode (matters for perf!)
+ options:
+ - "Fast (default)"
+ - "Accurate (Midsummer2)"
+ - "Both equally slow"
+ - "Tested fast only"
+ validations:
+ required: true
+
+ - type: input
+ id: core_version
+ attributes:
+ label: Core version
+ placeholder: "v2.2.0 abc1234"
+ validations:
+ required: true
+
+ - type: input
+ id: frontend
+ attributes:
+ label: Frontend + hardware
+ placeholder: "RetroArch 1.21.0, macOS 14.5, M2 Pro (arm64)"
+ validations:
+ required: true
+
+ - type: textarea
+ id: regression
+ attributes:
+ label: Did this work better before?
+ description: If yes, name the version it was fast in, or the commit/release that broke it. `git bisect` results welcome.
+
+ - type: checkboxes
+ id: pre
+ attributes:
+ label: Pre-flight
+ options:
+ - label: Verified by running with both `Fast` and `Accurate` blitter modes.
+ - label: Other libretro cores run at full speed on the same hardware (rules out general system slowness).
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..99e3406f
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,24 @@
+
+
+## Summary
+
+
+
+## Test plan
+
+
+
+- [ ] `make -j$(getconf _NPROCESSORS_ONLN)` builds clean on host
+- [ ] `make test` passes (or N/A — explain why)
+- [ ] `bash scripts/c89-lint.sh` passes (no mid-block declarations)
+
+## Branch base
+
+- [ ] **Base is `develop`** (the integration branch)
+- [ ] OR base is `master` *and* this is a `hotfix/*` or `release/*` branch
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..ebb85b84
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,26 @@
+# Dependabot keeps GitHub Action versions current so deprecation
+# warnings (Node 20 -> 24, etc.) surface as PRs instead of CI noise.
+# Targets `develop` to align with our GitFlow setup.
+
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ target-branch: "develop"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "07:00"
+ timezone: "America/New_York"
+ open-pull-requests-limit: 5
+ labels:
+ - "ci/cd :robot:"
+ - "dependencies"
+ commit-message:
+ prefix: "ci"
+ groups:
+ # One PR per Monday for routine action bumps.
+ actions-minor-patch:
+ update-types:
+ - "minor"
+ - "patch"
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 00000000..7341600d
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,120 @@
+# PR auto-labeler config consumed by .github/workflows/labeler.yml.
+# Schema: https://github.com/actions/labeler#create-labeleryml
+#
+# Goal: any PR that touches files in a subsystem gets the matching
+# label so subsystem-focused review and triage queries work
+# (`is:pr label:dsp`). Multiple labels per PR are normal.
+
+# --- Subsystems ---
+
+dsp:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/jerry/dsp*.c'
+ - 'src/jerry/dsp*.h'
+ - 'src/jerry/dsp_acc40.h'
+ - 'test/test_dsp_*.c'
+ - 'test/test_audio_*.c'
+
+gpu:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/tom/gpu.c'
+ - 'src/tom/gpu.h'
+ - 'test/test_gpu_*.c'
+
+blitter:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/tom/blitter*.c'
+ - 'src/tom/blitter*.h'
+ - 'test/test_blitter_*.c'
+ - 'test/tools/test_blitter_compare.c'
+
+op:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/tom/op.c'
+ - 'src/tom/op.h'
+ - 'test/test_op_*.c'
+
+m68k:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/m68000/**'
+ - 'test/test_m68k_*.c'
+
+cd:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/cd/**'
+ - 'test/test_cd_*.c'
+
+bios:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/bios/**'
+ - 'test/test_hle_bios.c'
+
+core:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/core/**'
+ - 'test/test_subsystem_*.c'
+ - 'test/test_event_*.c'
+ - 'test/test_irq_*.c'
+ - 'test/test_boot_*.c'
+
+# --- Areas ---
+
+libretro-glue:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'libretro.c'
+ - 'libretro_core_options.h'
+ - 'link.T'
+ - 'link-test.T'
+ - 'exports.list'
+ - 'exports-test.list'
+
+tests:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'test/**'
+
+build:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'Makefile'
+ - 'Makefile.common'
+ - 'scripts/**'
+ - 'jni/Android.mk'
+
+"ci/cd :robot:":
+ - changed-files:
+ - any-glob-to-any-file:
+ - '.github/workflows/**'
+ - '.github/dependabot.yml'
+ - '.github/labeler.yml'
+ - '.editorconfig'
+ - '.ecrc'
+ - '.clang-format'
+ - '.clang-tidy'
+ - 'cppcheck-suppressions.txt'
+ - 'gcovr.cfg'
+ - 'codecov.yml'
+
+":book: documentation":
+ - changed-files:
+ - any-glob-to-any-file:
+ - '**/*.md'
+ - 'docs/**'
+ - 'CLAUDE.md'
+ - 'CONTRIBUTING.md'
+
+performance:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'docs/profiling.md'
+ - 'src/tom/blitter_simd_*.c'
+ - 'test/tools/test_benchmark.c'
diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml
index 33ec94cf..ab2489ad 100644
--- a/.github/workflows/artifacts.yml
+++ b/.github/workflows/artifacts.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Comment artifact links on PR
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
const run = context.payload.workflow_run;
diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml
index 73ca5b56..7058b8b0 100644
--- a/.github/workflows/c-cpp.yml
+++ b/.github/workflows/c-cpp.yml
@@ -2,11 +2,18 @@ name: C/C++ CI
on:
push:
- branches: [ master ]
+ branches: [ master, develop ]
pull_request:
- branches: [ master ]
+ branches: [ develop, master ]
workflow_dispatch:
+# Cancel in-progress runs when a new commit lands on the same PR.
+# Pushes to master/develop run to completion (no cancellation) so we
+# always have a green-or-red record on the integration branches.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
jobs:
build:
strategy:
@@ -46,11 +53,16 @@ jobs:
cc: 'clang'
cxx: 'clang++'
- - displayTargetName: 'macOS x86_64 (Clang)'
+ # Cross-compiled from macos-latest (arm64) -- avoids the
+ # scarce macos-13 Intel runner pool. See release.yml for
+ # rationale.
+ - displayTargetName: 'macOS x86_64 (Clang, cross)'
artifact: 'virtualjaguar_libretro.dylib'
- os: macos-13
- cc: 'clang'
- cxx: 'clang++'
+ os: macos-latest
+ cc: 'clang -arch x86_64'
+ cxx: 'clang++ -arch x86_64'
+ make_extra: 'BLITTER_SIMD=sse2'
+ cross: true
# Windows (MinGW)
- displayTargetName: 'Windows x86_64 (MSYS2)'
@@ -123,7 +135,7 @@ jobs:
shell: ${{ matrix.config.shell || 'bash' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install multilib
if: matrix.config.multilib
@@ -142,7 +154,7 @@ jobs:
- name: Set up Emscripten
if: matrix.config.emscripten
- uses: mymindstorm/setup-emsdk@v14
+ uses: mymindstorm/setup-emsdk@v16
- name: Set up Android NDK
if: matrix.config.android
@@ -153,7 +165,7 @@ jobs:
- name: Build
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.make_platform }}
- run: make -j4 CC="${{ matrix.config.cc }}" CXX="${{ matrix.config.cxx }}"
+ run: make -j4 CC="${{ matrix.config.cc }}" CXX="${{ matrix.config.cxx }}" ${{ matrix.config.make_extra || '' }}
- name: Build (platform)
if: matrix.config.make_platform
@@ -166,9 +178,27 @@ jobs:
- name: Build (Android NDK)
if: matrix.config.android
run: |
+ # ndk-build uses jni/Android.mk (not the project Makefile),
+ # so the parse-time version.h regen in Makefile doesn't run.
+ bash scripts/gen-version-h.sh
${{ steps.setup-ndk.outputs.ndk-path }}/ndk-build \
APP_ABI=${{ matrix.config.android_abi }} -j4
+ # Mach-O symbol gating: production builds must export retro_* only.
+ # Runs before the test step (which would relink with TEST_EXPORTS=1
+ # and widen the symbol set).
+ - name: Verify Mach-O symbol gating (production)
+ if: ${{ runner.os == 'macOS' && !matrix.config.emscripten && !matrix.config.android }}
+ run: |
+ LEAKS=$(nm -gU "${{ matrix.config.artifact }}" | grep -v ' _retro_' || true)
+ if [ -n "$LEAKS" ]; then
+ echo "::error::Production dylib leaked non-retro_* symbols:"
+ echo "$LEAKS"
+ exit 1
+ fi
+ COUNT=$(nm -gU "${{ matrix.config.artifact }}" | wc -l | tr -d ' ')
+ echo "==> Production exports: ${COUNT} (all retro_*)"
+
# Host/native toolchains only — skips cross-compile rows (e.g. aarch64 on x86 runner).
- name: Run cheat engine unit tests
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
@@ -242,7 +272,7 @@ jobs:
- name: Cache pinned rcheevos E2E build
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: build/rcheevos-static
key: rcheevos-e2e-fd57e900-${{ runner.os }}-${{ matrix.config.cc }}-${{ matrix.config.displayTargetName }}
@@ -256,7 +286,7 @@ jobs:
run: bash test/tools/test_rcheevos_e2e.sh ./${{ matrix.config.artifact }}
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: ${{ matrix.config.displayTargetName }}
path: ${{ matrix.config.artifact }}
@@ -270,12 +300,16 @@ jobs:
matrix:
arch: [x64, x86]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.arch }}
+ - name: Generate src/core/version.h
+ shell: bash
+ run: bash scripts/gen-version-h.sh
+
- name: Compile all sources with cl.exe
shell: cmd
run: |
@@ -303,13 +337,13 @@ jobs:
container:
image: vitasdk/vitasdk:latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Build
run: make -j4 platform=vita
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: PS Vita
path: virtualjaguar_libretro_vita.a
@@ -321,7 +355,7 @@ jobs:
container:
image: devkitpro/devkita64:latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Build
run: make -j4 platform=libnx
@@ -329,23 +363,183 @@ jobs:
DEVKITPRO: /opt/devkitpro
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: Nintendo Switch
path: virtualjaguar_libretro_libnx.a
if-no-files-found: error
+ editorconfig-lint:
+ name: EditorConfig compliance check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install editorconfig-checker
+ run: |
+ VERSION=v3.0.3
+ curl -fsSL "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/${VERSION}/ec-linux-amd64.tar.gz" \
+ | tar -xz -C /tmp
+ sudo install -m 0755 /tmp/bin/ec-linux-amd64 /usr/local/bin/ec
+
+ - name: Run editorconfig-checker
+ run: ec -verbose
+
+ coverage:
+ name: Code coverage (gcov + codecov)
+ runs-on: ubuntu-latest
+ # Coverage runs are advisory -- they don't gate merges. The PR
+ # delta is what reviewers care about; codecov posts that as a
+ # comment automatically.
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Install gcov + gcovr
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y gcc gcovr
+ - name: make coverage
+ run: |
+ bash scripts/gen-version-h.sh
+ make coverage CC="gcc" CXX="g++"
+ - name: Upload to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ files: ./coverage.xml
+ fail_ci_if_error: false
+ # Token optional for public repos; if set in repo secrets it
+ # avoids the anonymous-upload rate limit.
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ sanitizers:
+ name: AddressSanitizer + UndefinedBehaviorSanitizer
+ runs-on: ubuntu-latest
+ # Initially advisory: lets the job report findings without blocking
+ # PRs while we triage the existing UB the cppcheck job already
+ # flagged (signed shifts in src/tom/{gpu,op}.c, etc.). Flip to
+ # `false` to make sanitizer findings gate merges.
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v5
+ - name: Install clang
+ run: sudo apt-get update && sudo apt-get install -y clang
+ - name: Build with ASAN + UBSAN
+ run: |
+ bash scripts/gen-version-h.sh
+ # -shared-libasan: when building a shared library with
+ # -fsanitize, clang doesn't link the static asan/ubsan
+ # runtime into the .so (it's expected to be loaded by the
+ # host executable). --no-undefined in the SHARED rule then
+ # rejects the runtime symbols. -shared-libasan switches to
+ # the dynamic libclang_rt.asan-x86_64.so which the host
+ # process picks up at dlopen time.
+ SAN_FLAGS="-fsanitize=address,undefined -shared-libasan -fno-omit-frame-pointer -O1 -g -fsanitize-ignorelist=$PWD/.ubsan-ignorelist"
+ make -j4 TEST_EXPORTS=1 \
+ CC="clang $SAN_FLAGS" \
+ CXX="clang++ $SAN_FLAGS" \
+ LD="clang $SAN_FLAGS"
+ - name: Run test suite under sanitizers
+ env:
+ ASAN_OPTIONS: 'detect_leaks=1:abort_on_error=1:print_stacktrace=1'
+ UBSAN_OPTIONS: 'print_stacktrace=1:halt_on_error=1:report_error_type=1'
+ run: |
+ SAN_FLAGS="-fsanitize=address,undefined -shared-libasan -fno-omit-frame-pointer -O1 -g -fsanitize-ignorelist=$PWD/.ubsan-ignorelist"
+ # The shared-libasan path means the test binaries need to find
+ # libclang_rt.asan at runtime; expose its directory.
+ ASAN_LIB_DIR=$(clang -print-resource-dir)/lib/linux
+ export LD_LIBRARY_PATH="$ASAN_LIB_DIR:${LD_LIBRARY_PATH:-}"
+ make test TEST_EXPORTS=1 \
+ CC="clang $SAN_FLAGS" \
+ CXX="clang++ $SAN_FLAGS" \
+ LD="clang $SAN_FLAGS"
+
+ clang-tidy:
+ name: clang-tidy on changed files
+ runs-on: ubuntu-latest
+ # Advisory at first. The .clang-tidy check list is curated, but
+ # the first pass over emulator C will surface noisy true positives
+ # that need triage. Flip continue-on-error to false once the
+ # baseline is calm.
+ continue-on-error: true
+ if: github.event_name == 'pull_request'
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Install clang-tidy + bear
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y clang-tidy bear
+ - name: Generate compile_commands.json
+ run: |
+ bash scripts/gen-version-h.sh
+ # `bear -- make` records every compile invocation into
+ # compile_commands.json without affecting the build.
+ bear -- make -j4
+ - name: Run clang-tidy on PR-changed files
+ run: |
+ BASE="${{ github.event.pull_request.base.sha }}"
+ FILES=$(git diff --name-only "$BASE"...HEAD -- '*.c' '*.h' \
+ | grep -Ev '^(src/m68000/|src/bios/jag.*\.c$|libretro-common/|src/core/version\.h$)' \
+ | grep -E '^(src/|libretro\.c$)' || true)
+ if [ -z "$FILES" ]; then
+ echo "No relevant C files changed; skipping clang-tidy."
+ exit 0
+ fi
+ echo "==> Running clang-tidy on:"
+ echo "$FILES"
+ echo "$FILES" | xargs clang-tidy -p . --quiet --warnings-as-errors='*'
+
+ cppcheck:
+ name: cppcheck static analysis
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install cppcheck
+ run: sudo apt-get update && sudo apt-get install -y cppcheck
+
+ - name: Run cppcheck
+ run: |
+ # Scan in-tree sources only. src/m68000 is machine-generated
+ # UAE 68K, src/bios/jag*.c are bin2c hex tables, and
+ # libretro-common is a vendored subtree -- all excluded.
+ #
+ # Suppressions live in cppcheck-suppressions.txt with rationale.
+ # cppcheck 2.13 (Ubuntu 24.04) doesn't accept comment lines in
+ # the suppressions file; strip them before passing.
+ bash scripts/gen-version-h.sh
+ # POSIX character class -- not GNU grep's \s extension.
+ grep -vE '^[[:space:]]*(#|$)' cppcheck-suppressions.txt > /tmp/cppcheck-suppressions.txt
+ cppcheck \
+ --enable=warning,performance,portability \
+ --suppress=missingIncludeSystem \
+ --suppress=unusedFunction \
+ --suppressions-list=/tmp/cppcheck-suppressions.txt \
+ --error-exitcode=1 \
+ -i src/m68000 -i src/bios -i libretro-common \
+ -I src -I src/core -I src/tom -I src/jerry -I src/cd \
+ -I src/bios -I src/m68000 -I libretro-common/include \
+ -DINLINE=inline -D__LIBRETRO__ \
+ --quiet \
+ src/ libretro.c
+
c89-lint:
name: C89 compliance check
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Check for declaration-after-statement
run: |
echo "==> Checking C89 compliance (catches MSVC C89 errors)..."
scripts/c89-lint.sh
+ - name: Verify .info display_version matches Makefile
+ run: bash scripts/check-info-version.sh
+
- name: Check for stdbool.h usage (use boolean.h instead)
run: |
echo "==> Checking for direct stdbool.h includes..."
diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml
new file mode 100644
index 00000000..0655e862
--- /dev/null
+++ b/.github/workflows/clang-format.yml
@@ -0,0 +1,83 @@
+name: clang-format check
+
+# Advisory check: runs clang-format on CHANGED LINES ONLY in the PR
+# diff, so the existing mixed-indent codebase isn't penalised. Posts
+# a job-summary diff if the new code deviates from `.clang-format`.
+#
+# Currently advisory (continue-on-error: true) so it doesn't block
+# merges while contributors get used to the convention. Flip to
+# gating once the team is ready.
+
+on:
+ pull_request:
+ branches: [develop, master]
+ paths: ['**/*.c', '**/*.h']
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ clang-format:
+ name: clang-format on changed lines
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Install clang-format
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y clang-format
+
+ - name: Run clang-format-diff on changed hunks
+ # Disable -e: git diff returns 1 when diff is non-empty,
+ # clang-format-diff returns 1 when it has suggestions, neither
+ # is a real failure here. We always exit 0 at the end.
+ shell: bash {0}
+ run: |
+ set -u
+ git fetch origin "${{ github.base_ref }}":__base
+ # Stream diff -> clang-format-diff -> file. Avoids capturing
+ # potentially large diffs into shell variables.
+ # -U0: zero context lines so clang-format-diff only sees
+ # added/changed lines. Pathspec excludes vendored/generated.
+ DIFF_FILE=$(mktemp)
+ git diff -U0 __base...HEAD -- \
+ ':(exclude)libretro-common/**' \
+ ':(exclude)src/m68000/**' \
+ ':(exclude)src/bios/jag*.c' \
+ ':(exclude)src/core/version.h' \
+ > "$DIFF_FILE"
+
+ # clang-format-diff ships in the Ubuntu clang-format package
+ # but isn't always on PATH; locate it.
+ CFD=$(command -v clang-format-diff || command -v clang-format-diff-18 || command -v clang-format-diff-17 || command -v clang-format-diff-16 || true)
+ if [ -z "$CFD" ]; then
+ echo "::warning::clang-format-diff not found; skipping advisory check."
+ exit 0
+ fi
+
+ OUT_FILE=$(mktemp)
+ "$CFD" -p1 -style=file < "$DIFF_FILE" > "$OUT_FILE" || true
+ if [ -s "$OUT_FILE" ]; then
+ echo "::notice::clang-format has style suggestions; see job summary."
+ {
+ echo "## clang-format suggestions"
+ echo
+ echo "These are advisory -- the codebase has mixed style per-file and"
+ echo "we do NOT auto-enforce. Apply if they make sense for your hunk."
+ echo
+ echo '```diff'
+ cat "$OUT_FILE"
+ echo '```'
+ } >> "$GITHUB_STEP_SUMMARY"
+ cat "$OUT_FILE"
+ else
+ echo "OK: no clang-format suggestions on changed lines"
+ fi
+ rm -f "$DIFF_FILE" "$OUT_FILE"
+ # Always exit 0 -- this check is informational only.
+ exit 0
diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml
deleted file mode 100644
index b5e8cfd4..00000000
--- a/.github/workflows/claude-code-review.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Claude Code Review
-
-on:
- pull_request:
- types: [opened, synchronize, ready_for_review, reopened]
- # Optional: Only run on specific file changes
- # paths:
- # - "src/**/*.ts"
- # - "src/**/*.tsx"
- # - "src/**/*.js"
- # - "src/**/*.jsx"
-
-jobs:
- claude-review:
- # Optional: Filter by PR author
- # if: |
- # github.event.pull_request.user.login == 'external-contributor' ||
- # github.event.pull_request.user.login == 'new-developer' ||
- # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
-
- runs-on: ubuntu-latest
- permissions:
- contents: read
- pull-requests: read
- issues: read
- id-token: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Run Claude Code Review
- id: claude-review
- uses: anthropics/claude-code-action@v1
- with:
- claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
- plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
- plugins: 'code-review@claude-code-plugins'
- prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
- # or https://code.claude.com/docs/en/cli-reference for available options
-
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index d300267f..5e721659 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 1
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 00000000..2ca2f6e2
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,22 @@
+name: PR labeler
+
+# Auto-applies subsystem / area labels based on the file paths a PR
+# touches. Configured in .github/labeler.yml. Idempotent: removes
+# labels that no longer apply if files are reverted.
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ label:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v5
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
+ sync-labels: true
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 75fc4a6f..625f3f5b 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -15,5 +15,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_PREFIX: 'feature/'
- PULL_REQUEST_BRANCH: 'master'
+ # GitFlow: feature/* branches integrate via develop.
+ PULL_REQUEST_BRANCH: 'develop'
PULL_REQUEST_DRAFT: true
diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml
index d607566a..7c2c55fe 100644
--- a/.github/workflows/rebase.yml
+++ b/.github/workflows/rebase.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
diff --git a/.github/workflows/regression-test.yml b/.github/workflows/regression-test.yml
index 65798f1a..efa9f006 100644
--- a/.github/workflows/regression-test.yml
+++ b/.github/workflows/regression-test.yml
@@ -2,9 +2,14 @@ name: Regression Tests
on:
push:
- branches: [ master ]
+ branches: [ master, develop ]
pull_request:
- branches: [ master ]
+ branches: [ develop, master ]
+
+# Cancel stale PR runs when new commits push.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
pull-requests: write
@@ -37,7 +42,7 @@ jobs:
runs-on: ${{ matrix.config.os }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install ImageMagick
run: |
@@ -49,7 +54,7 @@ jobs:
- name: Cache miniretro
id: cache-miniretro
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: miniretro-bin
key: miniretro-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.github/workflows/regression-test.yml') }}
@@ -82,7 +87,7 @@ jobs:
- name: Upload diff artifacts
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: regression-diffs-${{ matrix.config.platform }}
path: regression-diffs/
@@ -91,7 +96,7 @@ jobs:
- name: Comment on PR with results
if: always() && github.event_name == 'pull_request'
continue-on-error: true
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index afa2a09e..a6d8cbb0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,11 +46,17 @@ jobs:
cc: clang
cxx: clang++
+ # Cross-compiled from the macos-latest (arm64) runner via
+ # `clang -arch x86_64`. Avoids the increasingly scarce
+ # macos-13 (Intel) runner pool that left v2.2.0's release
+ # job queued for an hour. BLITTER_SIMD=sse2 is required
+ # because the Makefile's auto-detect uses host arch.
- platform: macos-x86_64
artifact: virtualjaguar_libretro.dylib
- os: macos-13
- cc: clang
- cxx: clang++
+ os: macos-latest
+ cc: clang -arch x86_64
+ cxx: clang++ -arch x86_64
+ make_extra: 'BLITTER_SIMD=sse2'
# Windows
- platform: windows-x86_64
@@ -121,7 +127,7 @@ jobs:
shell: ${{ matrix.config.shell || 'bash' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install multilib
if: matrix.config.multilib
@@ -140,7 +146,7 @@ jobs:
- name: Set up Emscripten
if: matrix.config.emscripten
- uses: mymindstorm/setup-emsdk@v14
+ uses: mymindstorm/setup-emsdk@v16
- name: Set up Android NDK
if: matrix.config.android
@@ -153,7 +159,7 @@ jobs:
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.make_platform }}
env:
RELEASE_DEBUG_INFO: '1'
- run: make -j4 CC="${{ matrix.config.cc }}" CXX="${{ matrix.config.cxx }}"
+ run: make -j4 CC="${{ matrix.config.cc }}" CXX="${{ matrix.config.cxx }}" ${{ matrix.config.make_extra || '' }}
- name: Build (platform)
if: matrix.config.make_platform
@@ -172,6 +178,9 @@ jobs:
env:
RELEASE_DEBUG_INFO: '1'
run: |
+ # ndk-build uses jni/Android.mk (not the project Makefile),
+ # so the parse-time version.h regen in Makefile doesn't run.
+ bash scripts/gen-version-h.sh
${{ steps.setup-ndk.outputs.ndk-path }}/ndk-build \
APP_ABI=${{ matrix.config.android_abi }} -j4
@@ -264,7 +273,7 @@ jobs:
rm "dist/${DBG}"
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: virtualjaguar_libretro-${{ matrix.config.platform }}
path: dist/
@@ -279,7 +288,7 @@ jobs:
container:
image: vitasdk/vitasdk:latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Build
env:
@@ -301,7 +310,7 @@ jobs:
rm "dist/${OUT}.debug"
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: virtualjaguar_libretro-vita
path: dist/
@@ -316,7 +325,7 @@ jobs:
container:
image: devkitpro/devkita64:latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Build
env:
@@ -337,7 +346,7 @@ jobs:
rm "dist/${OUT}.debug"
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: virtualjaguar_libretro-switch
path: dist/
@@ -352,10 +361,10 @@ jobs:
needs: [build, vita-build, switch-build]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Download all artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
path: artifacts/
diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml
index 7272307d..3bd00181 100644
--- a/.github/workflows/version-bump.yml
+++ b/.github/workflows/version-bump.yml
@@ -24,14 +24,15 @@ jobs:
bump:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Parse current version
id: current
run: |
- VER=$(grep -oP 'library_version\s*=\s*"v\K[0-9]+\.[0-9]+\.[0-9]+' libretro.c)
+ # Source of truth: CORE_BASE_VERSION line in Makefile.
+ VER=$(grep -oP '^CORE_BASE_VERSION\s*:?=\s*v\K[0-9]+\.[0-9]+\.[0-9]+' Makefile)
echo "version=${VER}" >> "$GITHUB_OUTPUT"
echo "Current version: v${VER}"
@@ -48,17 +49,17 @@ jobs:
echo "version=${NEXT}" >> "$GITHUB_OUTPUT"
echo "Next version: v${NEXT}"
- - name: Update libretro.c
+ - name: Update Makefile
run: |
- sed -i 's/library_version = "v${{ steps.current.outputs.version }}"/library_version = "v${{ steps.next.outputs.version }}"/' libretro.c
- grep 'library_version' libretro.c
+ sed -i 's/^CORE_BASE_VERSION := v${{ steps.current.outputs.version }}$/CORE_BASE_VERSION := v${{ steps.next.outputs.version }}/' Makefile
+ grep '^CORE_BASE_VERSION' Makefile
- name: Commit, tag, and push
if: ${{ !inputs.dry_run }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- git add libretro.c
+ git add Makefile
git commit -m "Bump version to v${{ steps.next.outputs.version }}"
git tag "v${{ steps.next.outputs.version }}"
git push origin HEAD --tags
diff --git a/.github/workflows/warn-pr-base.yml b/.github/workflows/warn-pr-base.yml
new file mode 100644
index 00000000..ac864fae
--- /dev/null
+++ b/.github/workflows/warn-pr-base.yml
@@ -0,0 +1,65 @@
+name: Warn on PR base
+
+# Posts a friendly comment on PRs that target master directly, asking
+# the contributor to retarget to develop (the integration branch under
+# our GitFlow setup).
+#
+# Skips PRs from `release/*` and `hotfix/*` branches, since those are
+# legitimately master-bound (the GitFlow exit ramps).
+#
+# Uses pull_request_target so we get write perms on PRs from forks
+# (the action only reads repository metadata, never executes PR code,
+# so this is safe).
+
+on:
+ pull_request_target:
+ types: [opened, reopened, edited]
+ branches: [master]
+
+permissions:
+ pull-requests: write
+
+jobs:
+ warn:
+ runs-on: ubuntu-latest
+ if: |
+ !startsWith(github.event.pull_request.head.ref, 'release/') &&
+ !startsWith(github.event.pull_request.head.ref, 'hotfix/')
+ steps:
+ - name: Comment on PR
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const marker = '';
+ const body = [
+ marker,
+ '## Heads up: this PR targets `master`',
+ '',
+ 'We use a [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/)-style integration branch. Could you retarget this PR to **`develop`**? You can do that here without re-opening:',
+ '',
+ '```bash',
+ `gh pr edit ${context.payload.pull_request.number} --base develop`,
+ '```',
+ '',
+ 'Or click *Edit* next to the title in the GitHub UI and change the base.',
+ '',
+ '`master` is reserved for release / hotfix branches (`release/*`, `hotfix/*`); everything else flows through `develop` first. Thanks!',
+ ].join('\n');
+
+ // Avoid spamming on edits: only post once per PR.
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.payload.pull_request.number,
+ });
+ if (comments.some(c => (c.body || '').includes(marker))) {
+ core.info('warn-pr-base comment already present; skipping.');
+ return;
+ }
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.payload.pull_request.number,
+ body,
+ });
diff --git a/.gitignore b/.gitignore
index a3ecb0ab..048568e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,9 @@
*.js
*.bc
+# Generated by scripts/gen-version-h.sh on every build
+src/core/version.h
+
# macOS
.DS_Store
.build
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0ba4e9cd..50dc350f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,229 +1,229 @@
-# DESCRIPTION: GitLab CI/CD for libRetro (NOT FOR GitLab-proper)
-
-##############################################################################
-################################# BOILERPLATE ################################
-##############################################################################
-
-# Core definitions
-.core-defs:
- variables:
- JNI_PATH: .
- CORENAME: virtualjaguar
-
-# Inclusion templates, required for the build to work
-include:
- ################################## DESKTOPS ################################
- # Windows 64-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/windows-x64-mingw.yml'
-
- # Windows 32-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/windows-i686-mingw.yml'
-
- # Windows msvc10 64-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/windows-x64-msvc10-msys2.yml'
-
- # Windows msvc10 32-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/windows-i686-msvc10-msys2.yml'
-
- # Windows msvc05 32-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/windows-i686-msvc05-msys2.yml'
-
- # Linux 64-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/linux-x64.yml'
-
- # Linux 32-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/linux-i686.yml'
-
- # Linux 64-bit (ARM)
- - project: 'libretro-infrastructure/ci-templates'
- file: '/linux-aarch64.yml'
-
- # MacOS 64-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/osx-x64.yml'
-
- # MacOS ARM 64-bit
- - project: 'libretro-infrastructure/ci-templates'
- file: '/osx-arm64.yml'
-
- ################################## CELLULAR ################################
- # Android
- - project: 'libretro-infrastructure/ci-templates'
- file: '/android-jni.yml'
-
- # iOS
- - project: 'libretro-infrastructure/ci-templates'
- file: '/ios-arm64.yml'
-
- # iOS (armv7)
- - project: 'libretro-infrastructure/ci-templates'
- file: '/ios9.yml'
-
- ################################## CONSOLES ################################
- # PlayStation Vita
- - project: 'libretro-infrastructure/ci-templates'
- file: '/vita-static.yml'
-
- # Nintendo Switch
- - project: 'libretro-infrastructure/ci-templates'
- file: '/libnx-static.yml'
-
- # tvOS (AppleTV)
- - project: 'libretro-infrastructure/ci-templates'
- file: '/tvos-arm64.yml'
-
- #################################### MISC ##################################
- # Emscripten (WebAssembly)
- - project: 'libretro-infrastructure/ci-templates'
- file: '/emscripten-static.yml'
-
- # webOS 32-bit (LGTV)
- - project: 'libretro-infrastructure/ci-templates'
- file: '/webos-make.yml'
-
-# Stages for building
-stages:
- - build-prepare
- - build-shared
- - build-static
-
-##############################################################################
-#################################### STAGES ##################################
-##############################################################################
-#
-################################### DESKTOPS #################################
-# Windows 64-bit
-libretro-build-windows-x64:
- extends:
- - .libretro-windows-x64-mingw-make-default
- - .core-defs
-
-# Windows 32-bit
-libretro-build-windows-i686:
- extends:
- - .libretro-windows-i686-mingw-make-default
- - .core-defs
-
-# Windows msvc10 64-bit
-libretro-build-windows-msvc10-x64:
- extends:
- - .libretro-windows-x64-msvc10-msys2-make-default
- - .core-defs
-
-# Windows msvc10 32-bit
-libretro-build-windows-msvc10-i686:
- extends:
- - .libretro-windows-i686-msvc10-msys2-make-default
- - .core-defs
-
-# Windows msvc05 32-bit
-libretro-build-windows-msvc05-i686:
- extends:
- - .libretro-windows-i686-msvc05-msys2-make-default
- - .core-defs
-
-# Linux 64-bit
-libretro-build-linux-x64:
- extends:
- - .libretro-linux-x64-make-default
- - .core-defs
-
-# Linux 32-bit
-libretro-build-linux-i686:
- extends:
- - .libretro-linux-i686-make-default
- - .core-defs
-
-# Linux 64-bit (ARM)
-libretro-build-linux-aarch64:
- extends:
- - .libretro-linux-aarch64-make-default
- - .core-defs
-
-# MacOS 64-bit
-libretro-build-osx-x64:
- extends:
- - .libretro-osx-x64-make-default
- - .core-defs
-
-# MacOS ARM 64-bit
-libretro-build-osx-arm64:
- extends:
- - .libretro-osx-arm64-make-default
- - .core-defs
-
-################################### CELLULAR #################################
-# Android ARMv7a
-android-armeabi-v7a:
- extends:
- - .libretro-android-jni-armeabi-v7a
- - .core-defs
-
-# Android ARMv8a
-android-arm64-v8a:
- extends:
- - .libretro-android-jni-arm64-v8a
- - .core-defs
-
-# Android 64-bit x86
-android-x86_64:
- extends:
- - .libretro-android-jni-x86_64
- - .core-defs
-
-# Android 32-bit x86
-android-x86:
- extends:
- - .libretro-android-jni-x86
- - .core-defs
-
-# iOS
-libretro-build-ios-arm64:
- extends:
- - .libretro-ios-arm64-make-default
- - .core-defs
-
-# iOS (armv7) [iOS 9 and up]
-libretro-build-ios9:
- extends:
- - .libretro-ios9-make-default
- - .core-defs
-
-# tvOS
-libretro-build-tvos-arm64:
- extends:
- - .libretro-tvos-arm64-make-default
- - .core-defs
-
-################################### CONSOLES #################################
-# PlayStation Vita
-libretro-build-vita:
- extends:
- - .libretro-vita-static-retroarch-master
- - .core-defs
-
-# Nintendo Switch
-libretro-build-libnx-aarch64:
- extends:
- - .libretro-libnx-static-retroarch-master
- - .core-defs
-
-#################################### MISC ####################################
-# Emscripten (WebAssembly)
-libretro-build-emscripten:
- extends:
- - .libretro-emscripten-static-retroarch-master
- - .core-defs
-
-# webOS 32-bit
-libretro-build-webos-armv7a:
- extends:
- - .libretro-webos-armv7a-make-default
- - .core-defs
+# DESCRIPTION: GitLab CI/CD for libRetro (NOT FOR GitLab-proper)
+
+##############################################################################
+################################# BOILERPLATE ################################
+##############################################################################
+
+# Core definitions
+.core-defs:
+ variables:
+ JNI_PATH: .
+ CORENAME: virtualjaguar
+
+# Inclusion templates, required for the build to work
+include:
+ ################################## DESKTOPS ################################
+ # Windows 64-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/windows-x64-mingw.yml'
+
+ # Windows 32-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/windows-i686-mingw.yml'
+
+ # Windows msvc10 64-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/windows-x64-msvc10-msys2.yml'
+
+ # Windows msvc10 32-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/windows-i686-msvc10-msys2.yml'
+
+ # Windows msvc05 32-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/windows-i686-msvc05-msys2.yml'
+
+ # Linux 64-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/linux-x64.yml'
+
+ # Linux 32-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/linux-i686.yml'
+
+ # Linux 64-bit (ARM)
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/linux-aarch64.yml'
+
+ # MacOS 64-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/osx-x64.yml'
+
+ # MacOS ARM 64-bit
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/osx-arm64.yml'
+
+ ################################## CELLULAR ################################
+ # Android
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/android-jni.yml'
+
+ # iOS
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/ios-arm64.yml'
+
+ # iOS (armv7)
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/ios9.yml'
+
+ ################################## CONSOLES ################################
+ # PlayStation Vita
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/vita-static.yml'
+
+ # Nintendo Switch
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/libnx-static.yml'
+
+ # tvOS (AppleTV)
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/tvos-arm64.yml'
+
+ #################################### MISC ##################################
+ # Emscripten (WebAssembly)
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/emscripten-static.yml'
+
+ # webOS 32-bit (LGTV)
+ - project: 'libretro-infrastructure/ci-templates'
+ file: '/webos-make.yml'
+
+# Stages for building
+stages:
+ - build-prepare
+ - build-shared
+ - build-static
+
+##############################################################################
+#################################### STAGES ##################################
+##############################################################################
+#
+################################### DESKTOPS #################################
+# Windows 64-bit
+libretro-build-windows-x64:
+ extends:
+ - .libretro-windows-x64-mingw-make-default
+ - .core-defs
+
+# Windows 32-bit
+libretro-build-windows-i686:
+ extends:
+ - .libretro-windows-i686-mingw-make-default
+ - .core-defs
+
+# Windows msvc10 64-bit
+libretro-build-windows-msvc10-x64:
+ extends:
+ - .libretro-windows-x64-msvc10-msys2-make-default
+ - .core-defs
+
+# Windows msvc10 32-bit
+libretro-build-windows-msvc10-i686:
+ extends:
+ - .libretro-windows-i686-msvc10-msys2-make-default
+ - .core-defs
+
+# Windows msvc05 32-bit
+libretro-build-windows-msvc05-i686:
+ extends:
+ - .libretro-windows-i686-msvc05-msys2-make-default
+ - .core-defs
+
+# Linux 64-bit
+libretro-build-linux-x64:
+ extends:
+ - .libretro-linux-x64-make-default
+ - .core-defs
+
+# Linux 32-bit
+libretro-build-linux-i686:
+ extends:
+ - .libretro-linux-i686-make-default
+ - .core-defs
+
+# Linux 64-bit (ARM)
+libretro-build-linux-aarch64:
+ extends:
+ - .libretro-linux-aarch64-make-default
+ - .core-defs
+
+# MacOS 64-bit
+libretro-build-osx-x64:
+ extends:
+ - .libretro-osx-x64-make-default
+ - .core-defs
+
+# MacOS ARM 64-bit
+libretro-build-osx-arm64:
+ extends:
+ - .libretro-osx-arm64-make-default
+ - .core-defs
+
+################################### CELLULAR #################################
+# Android ARMv7a
+android-armeabi-v7a:
+ extends:
+ - .libretro-android-jni-armeabi-v7a
+ - .core-defs
+
+# Android ARMv8a
+android-arm64-v8a:
+ extends:
+ - .libretro-android-jni-arm64-v8a
+ - .core-defs
+
+# Android 64-bit x86
+android-x86_64:
+ extends:
+ - .libretro-android-jni-x86_64
+ - .core-defs
+
+# Android 32-bit x86
+android-x86:
+ extends:
+ - .libretro-android-jni-x86
+ - .core-defs
+
+# iOS
+libretro-build-ios-arm64:
+ extends:
+ - .libretro-ios-arm64-make-default
+ - .core-defs
+
+# iOS (armv7) [iOS 9 and up]
+libretro-build-ios9:
+ extends:
+ - .libretro-ios9-make-default
+ - .core-defs
+
+# tvOS
+libretro-build-tvos-arm64:
+ extends:
+ - .libretro-tvos-arm64-make-default
+ - .core-defs
+
+################################### CONSOLES #################################
+# PlayStation Vita
+libretro-build-vita:
+ extends:
+ - .libretro-vita-static-retroarch-master
+ - .core-defs
+
+# Nintendo Switch
+libretro-build-libnx-aarch64:
+ extends:
+ - .libretro-libnx-static-retroarch-master
+ - .core-defs
+
+#################################### MISC ####################################
+# Emscripten (WebAssembly)
+libretro-build-emscripten:
+ extends:
+ - .libretro-emscripten-static-retroarch-master
+ - .core-defs
+
+# webOS 32-bit
+libretro-build-webos-armv7a:
+ extends:
+ - .libretro-webos-armv7a-make-default
+ - .core-defs
diff --git a/.ubsan-ignorelist b/.ubsan-ignorelist
new file mode 100644
index 00000000..6cfc89e5
--- /dev/null
+++ b/.ubsan-ignorelist
@@ -0,0 +1,11 @@
+# UBSAN ignorelist (clang -fsanitize-ignorelist=).
+#
+# src/m68000/ is the UAE 68000 emulator (machine-generated, ~1.8 MB
+# cpuemu.c). It uses negative shifts and other UB idioms intentionally
+# (the generator emits them; fixing requires patching the generator
+# upstream). We treat the directory as opaque, so UBSAN findings inside
+# it are noise that hides real issues in our own code.
+#
+# Add other entries here ONLY with a one-line comment justifying why.
+
+src:src/m68000/*
diff --git a/CLAUDE.md b/CLAUDE.md
index d3064983..12939ace 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,125 +1,89 @@
# CLAUDE.md
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+Guidance for Claude Code working in this repository.
-## Project Overview
+## Project
-Virtual Jaguar libretro core — an Atari Jaguar emulator ported to the libretro API. Written in C, licensed under GPLv3. Upstream: `http://shamusworld.gotdns.org/git/virtualjaguar`.
+Virtual Jaguar libretro core — Atari Jaguar emulator on the libretro API. C, GPLv3. Upstream: `http://shamusworld.gotdns.org/git/virtualjaguar`.
-## Build Commands
+## Branching
-```bash
-make -j$(getconf _NPROCESSORS_ONLN) # Build (auto-detects platform)
-make -j$(getconf _NPROCESSORS_ONLN) DEBUG=1 # Debug build (-O0 -g)
-make clean # Clean build artifacts
-make platform=ios-arm64 # Cross-compile for specific platform
-```
-
-Output binary name varies by platform:
-- macOS: `virtualjaguar_libretro.dylib`
-- Linux: `virtualjaguar_libretro.so`
-- Windows: `virtualjaguar_libretro.dll`
-
-CI runs `make -j4` on Ubuntu (GCC) and macOS (Clang), plus screenshot regression tests via `test/regression_test.sh`. See `docs/test-infrastructure.md` for the full test harness inventory.
-
-## Architecture
-
-### C Language Standard — C89/GNU89
+GitFlow: branch new work off **`develop`** (the integration branch); `master` is release-only (tagged commits, hotfix merges, release-branch merges). PRs targeting `master` get auto-warned by `.github/workflows/warn-pr-base.yml` — retarget to `develop` unless the source branch is `hotfix/*` or `release/*`. Full flow in [`docs/release-process.md`](docs/release-process.md).
-This codebase **must** compile as C89 (GNU89 dialect). The libretro buildbot uses MSVC on Windows, which enforces C89 strictly. CI includes a `c89-lint` job that catches violations.
+## Build
-**Rules:**
-- **No mid-block variable declarations.** All variables must be declared at the top of their enclosing block (function or `{}`), before any statements. This is the most common violation.
-- `//` comments are allowed (GNU89 extension), but `/* */` is preferred for new code.
-- No C99 features: no `for (int i = ...)`, no compound literals, no designated initializers, no VLAs.
-- SIMD files (`src/tom/blitter_simd_sse2.c`, `src/tom/blitter_simd_neon.c`) are exempt from the lint check since they require platform-specific headers.
-- Machine-generated files (`src/m68000/*`) are also exempt.
-
-**Local check before pushing:**
```bash
-gcc -fsyntax-only -std=gnu89 -Werror=declaration-after-statement \
- -I. -Isrc -Isrc/core -Isrc/tom -Isrc/jerry -Isrc/cd -Isrc/bios -Isrc/m68000 -Ilibretro-common/include \
- -D__LIBRETRO__ -DINLINE="inline" src/YOURFILE.c
+make -j$(getconf _NPROCESSORS_ONLN) # Build (auto-detects platform)
+make -j$(getconf _NPROCESSORS_ONLN) DEBUG=1 # Debug (-O0 -g)
+make clean
+make platform=ios-arm64 # Cross-compile target
```
-### Atari Jaguar Hardware Emulation
-
-The Jaguar has four processors sharing a unified memory-mapped address space:
-
-- **Motorola 68000** (13.3 MHz) — main CPU for game logic. Emulated via UAE-derived core in `src/m68000/`. The `cpuemu.c` file is machine-generated and very large (~1.8 MB).
-- **GPU** (26.6 MHz RISC) — graphics coprocessor in `src/tom/gpu.c`
-- **DSP** (26.6 MHz RISC) — audio coprocessor in `src/jerry/dsp.c`, same instruction set as GPU
-- **Object Processor** — sprite/bitmap rendering in `src/tom/op.c`
-
-Two custom chips contain these processors:
-- **TOM** (`src/tom/tom.c`) — video output, GPU, Object Processor, Blitter (`src/tom/blitter.c`)
-- **JERRY** (`src/jerry/jerry.c`) — audio DAC (`src/jerry/dac.c`), DSP, timers, EEPROM (`src/jerry/eeprom.c`)
-
-### Execution Model
-
-Frame execution is event-driven, not cycle-accurate. `JaguarExecuteNew()` in `src/core/jaguar.c` runs the main loop: the 68K executes until the next timed event, then GPU runs for the same timeslice, then event callbacks fire (half-line rendering, timer interrupts, etc.).
+Output: `virtualjaguar_libretro.{dylib,so,dll}`. CI: `make -j4` on Ubuntu (GCC) and macOS (Clang) plus `test/regression_test.sh` screenshots.
-### Memory
+## C89 / GNU89 — strict
-Memory map defined in `src/core/vjag_memory.h`. The Jaguar is big-endian; `GET16/GET32/SET16/SET32` macros handle byte-swapping on little-endian hosts. Main RAM is 2 MB at 0x000000, cart ROM at 0x800000, TOM registers at 0xF00000, JERRY registers at 0xF10000.
+The libretro buildbot uses MSVC on Windows. CI has a `c89-lint` job. Run `bash scripts/c89-lint.sh src/YOURFILE.c` before pushing.
-### Libretro Integration
+- **No mid-block declarations.** All vars at top of block, before any statement. Most common violation.
+- `//` comments allowed (GNU89), but prefer `/* */` for new code.
+- No C99: no `for (int i…)`, no compound literals, no designated initializers, no VLAs.
+- Exempt (see `scripts/c89-lint.sh::skip_file`): `src/m68000/cpu*.c` and `src/m68000/read*.c` (UAE 68K), `src/bios/jag*bios*.c` and `src/bios/jagstub*bios.c` (bin2c hex tables), `src/tom/blitter_simd_{sse2,neon}.c` (platform intrinsics), `test/tools/test_rcheevos_e2e.c` (rcheevos-dependent), `test/tools/flicker_detect.c` (diagnostic).
-`libretro.c` (top-level) implements the libretro API — initialization, per-frame execution, input polling, video/audio output. Video is XRGB8888 at dynamic resolution (typically 320x240 NTSC / 320x256 PAL). Audio is 48 kHz 16-bit stereo.
+## Hardware model
-Core options defined in `libretro_core_options.h` control blitter mode, BIOS usage, NTSC/PAL, DSP execution, and input mapping.
+Four processors, unified memory map, big-endian. `GET16/GET32/SET16/SET32` macros byte-swap on LE hosts. Address-range map is documented in `src/core/vjag_memory.c` (header comment); the dispatch logic lives in `src/core/jaguar.c`. RAM 0x000000 (2 MB), cart 0x800000, TOM regs 0xF00000, JERRY regs 0xF10000.
-### Key Directories
+- **68000** (13.3 MHz, `src/m68000/`) — main CPU. UAE-derived. `cpuemu.c` is **machine-generated, ~1.8 MB** — never read whole; grep first, then `Read` with offset/limit only on matched ranges.
+- **GPU** (26.6 MHz RISC, `src/tom/gpu.c`) — graphics coprocessor.
+- **DSP** (`src/jerry/dsp.c`) — same ISA as GPU; audio.
+- **Object Processor** (`src/tom/op.c`) — sprite/bitmap rendering.
+- **TOM** (`src/tom/tom.c`) — video, GPU, OP, Blitter (`src/tom/blitter.c`).
+- **JERRY** (`src/jerry/jerry.c`) — audio DAC, DSP, timers, EEPROM.
-- `src/core/` — top-level emulator orchestration, memory map, events, settings, files, cheats
-- `src/tom/` — TOM-side video, GPU, Object Processor, blitter, and blitter SIMD
-- `src/jerry/` — JERRY-side audio, DSP, DAC, EEPROM, input, wavetable
-- `src/cd/` — Jaguar CD/BUTCH and disc-interface layer
-- `src/bios/` — embedded BIOS and boot stub arrays
-- `src/m68000/` — UAE-derived 68K CPU emulation
-- `libretro-common/` — shared libretro utility library (string, file, VFS)
-- `docs/` — documentation: changelog, known issues, BUTCH register map, CD data flow, test infrastructure
-- `test/tools` — test scripts and headless front-ends
-- `test/roms` — test ROMs; `private/` subdirectory has commercial ROMs and BIOSes
+Frame loop is event-driven (not cycle-accurate): `JaguarExecuteNew()` in `src/core/jaguar.c` runs 68K to next event, then GPU, then fires callbacks (half-line render, timers).
-### Build System
+## Libretro layer
-`Makefile` handles 30+ platform targets with auto-detection. `Makefile.common` lists all source files. Platform is selected via `platform=` variable or auto-detected from `uname`. Key flags: `-D__LIBRETRO__`, `-DMSB_FIRST` for big-endian platforms.
+`libretro.c` (top-level) implements the API. Video XRGB8888 dynamic res (320×240 NTSC / 320×256 PAL). Audio 48 kHz 16-bit stereo. Core options in `libretro_core_options.h` (blitter mode, BIOS, NTSC/PAL, DSP, input).
-### Jaguar CD Emulation
+## Layout
-CD support is implemented across `src/cd/cdrom.c` (BUTCH chip / FIFO / DSA commands), `src/cd/cdintf.c` (disc image loading: CUE/BIN, CHD, CDI), and hooks in `src/core/jaguar.c` (BIOS auth bypass, boot stub injection).
+- `src/core/` — orchestration, memory map, events, settings, files, cheats
+- `src/tom/` — video, GPU, OP, blitter (+ SIMD)
+- `src/jerry/` — audio, DSP, DAC, EEPROM, input, wavetable
+- `src/cd/` — Jaguar CD: BUTCH/FIFO/DSA in `cdrom.c`, image loading (CUE/BIN, CHD, CDI) in `cdintf.c`; BIOS auth bypass + boot stub in `src/core/jaguar.c`
+- `src/bios/` — embedded BIOS / boot stubs
+- `src/m68000/` — UAE 68K (machine-generated; treat as opaque)
+- `libretro-common/` — shared utility lib
+- `test/tools/` — test harnesses; `test/roms/private/` — commercial ROMs/BIOSes (gitignored)
-Key docs:
-- `docs/butch-registers.md` — full BUTCH register map ($DFFF00-$DFFF2F) with bit definitions
-- `docs/cd-data-flow.md` — how CD data moves from disc to RAM (I2S -> FIFO -> GPU ISR -> RAM), BIOS code map, boot stub layout
+## Build system
-### Testing
+`Makefile` covers 30+ targets, auto-detected via `uname` or `platform=`. `Makefile.common` lists sources. Flags: `-D__LIBRETRO__`, `-DMSB_FIRST` for big-endian.
-RetroAchievements-related — **no RetroAchievements account, API, or gameplay server**; local validation only. The E2E harness still **fetches the pinned rcheevos source tarball from GitHub** when `build/rcheevos-static` is missing (CI may cache that directory); that is unrelated to contacting RetroAchievements services.
+## Testing
-- `test/tools/test_memory_map.c` — asserts `SET_MEMORY_MAPS`, `SET_SUPPORT_ACHIEVEMENTS` with **`true`**, and descriptor layout vs `retro_get_memory_data(SYSTEM_RAM)`.
-- `test/tools/test_rcheevos_e2e.sh` — downloads pinned **rcheevos** (`RCHEEVOS_REF`) when needed, builds `librcheevos.a`, then runs `test_rcheevos_e2e` to verify **rc_libretro** memory resolution (`RC_CONSOLE_ATARI_JAGUAR`) matches host RAM — the same mapping stack RetroArch uses before any RA cloud call.
+Local-only RetroAchievements validation — no RA account/API/server. `test/tools/test_rcheevos_e2e.sh` downloads pinned `RCHEEVOS_REF` and verifies `rc_libretro` mapping (`RC_CONSOLE_ATARI_JAGUAR`) matches host RAM.
-See `docs/test-infrastructure.md` for all test harnesses:
-- `test/test_dsp_mac40.c` — Jaguar DSP **40-bit MAC** accumulator semantics (`dsp_acc40.h`), run in CI with SIMD tests; relevant for long IIR chains (e.g. pink-noise generators on DSP).
-- `test/headless.py` — Python headless runner via libretro.py (screenshots, frame control)
-- `test/regression_test.sh` — screenshot regression suite with baseline comparison
-- `test/test_cd_boot.c` — low-level C harness with dlsym access to 68K registers and RAM
-- `test/sram_test.sh` — SRAM interface round-trip testing
+Key harnesses:
+- `test/regression_test.sh` — screenshot regression vs `test/baselines/` via miniretro (built from source on first run; `MINIRETRO_BIN` env to skip the build)
+- `test/tools/test_memory_map.c` — asserts `SET_MEMORY_MAPS`, `SET_SUPPORT_ACHIEVEMENTS=true`, descriptor layout
+- `test/tools/test_blitter_compare` — fast vs accurate blitter diff
+- `test/test_dsp_mac40.c` — DSP 40-bit MAC accumulator (`dsp_acc40.h`)
+- `test/test_cd_boot.c` — dlsym harness for 68K regs/RAM
+- `test/sram_test.sh` — SRAM round-trip
-#### Headless framebuffer / 240p suite — how to report issues
+### Performance / profiling
-Profiling symbols like **`__muldi3`** (64-bit multiply helpers on some 32-bit ABIs) are a **compiler/performance** concern, **not** evidence that the Jaguar 240p test ROM’s DSP pink-noise path or NTSC timing is wrong. Do **not** frame “240p fails” primarily as a **`__muldi3`** bug unless you are optimizing a 32-bit build for speed.
+`make benchmark` runs `test/tools/test_benchmark` headlessly against a fixed ROM (default `test/roms/yarc.j64`, 600 frames) and prints FPS / ms-per-frame. Use as a same-host commit-to-commit delta — don't compare across machines. Full guide: [`docs/profiling.md`](docs/profiling.md) covers Instruments / `perf` / flame graphs and the SIMD A/B knob.
-The **useful** regression story for automated screenshot / libretro.py / SessionBuilder runs is:
+### Headless framebuffer caveat
-- **Symptom:** On some cores or builds, a **non-RetroArch headless session** does not expose the **same composited framebuffer** via the libretro API (`video_screenshot`, etc.) as **RetroArch with the same core binary** — e.g. main menu of **jag_240p_test_suite v1.0.0** shows only a **thin band** (~order of **1k** non-black RGB pixels) vs **tens of thousands** on a known-good path.
-- **Interpretation:** Suspect **presentation / pixel source** — Object Processor and blitter output vs **what headless clients actually read** — until disproven with hardware or reference captures. This is **not** “prove 240p timing is wrong first.”
-- **Checks:** Use in-repo gates (e.g. **screenshots-preflight** / main-menu sanity, non-black pixel floor on the **~2000+** scale). Passing preflight ⇒ the **headless read-path** issue is resolved for that artifact; failing preflight ⇒ file a **framebuffer/compositing** bug for **headless libretro** consumption (logs, **two artifacts**: broken vs good), not a long **`__muldi3`** narrative.
+The miniretro harness used by `test/regression_test.sh` doesn't expose the same composited framebuffer that RetroArch reads. Symptom: `jag_240p_test_suite` main menu shows ~1k non-black pixels via miniretro vs tens of thousands via RetroArch. Treat that as a **headless read-path / presentation bug** (OP+blitter output vs what the host reads), not a 240p timing or `__muldi3` performance bug. Verify against RetroArch before treating a regression as real.
-### Known Limitations
+## Known limitations
-- Blitter not fully cycle-accurate (some games need fast blitter mode)
-- Bus contention between processors not emulated
-- Vertical count (VC) register behavior not fully accurate
+- Blitter not fully cycle-accurate (some games need fast mode).
+- No bus contention modeling.
+- VC register behavior not fully accurate.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..7dbf6b75
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,84 @@
+# Contributing
+
+Thanks for considering a contribution. This guide covers the practical mechanics — branching, build, test, lint — that keep CI green and reviews fast. For architecture context, see [`CLAUDE.md`](CLAUDE.md).
+
+## Branching (GitFlow)
+
+This repo uses a [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/)-style layout:
+
+| Branch | Role |
+|---------------|-------------------------------------------------------------------------|
+| `master` | Release-only. Tagged commits, hotfix merges, release-branch merges. |
+| `develop` | Integration branch. **Default base for new PRs.** |
+| `feature/*` | New work. Branch from `develop`, PR back into `develop`. |
+| `release/X.Y.Z` | Release stabilization. Branched from `develop`, merged into `master`. |
+| `hotfix/X.Y.Z` | Urgent fix off a tagged release. Branched from `master`, back-merged to `develop`. |
+
+PRs targeting `master` directly trigger an auto-comment from `.github/workflows/warn-pr-base.yml` asking you to retarget to `develop` (skipped automatically if your branch is `release/*` or `hotfix/*`).
+
+```bash
+git checkout develop
+git pull libretro develop
+git checkout -b feature/my-thing
+# ... commits ...
+git push -u origin feature/my-thing
+gh pr create --base develop
+```
+
+## Build
+
+```bash
+make -j$(getconf _NPROCESSORS_ONLN) # default platform (auto-detect)
+make -j$(getconf _NPROCESSORS_ONLN) DEBUG=1 # -O0 -g
+make platform=ios-arm64 # cross-compile target
+make clean
+```
+
+Output: `virtualjaguar_libretro.{dylib,so,dll}` at the repo root.
+
+## Test
+
+```bash
+make test # full white-box test suite
+./test/regression_test.sh ./virtualjaguar_libretro.so # screenshot regression vs test/baselines/
+make benchmark # headless wall-clock perf (see docs/profiling.md)
+```
+
+`make test` re-invokes the build with `TEST_EXPORTS=1` (link-test.T / exports-test.list) so the test binaries can `dlsym` into the emulator internals. `make` (default) restores the production-slim symbol set.
+
+## Lint (run before pushing)
+
+CI runs all of these as gates. Run them locally to avoid round-trips:
+
+```bash
+bash scripts/c89-lint.sh # mid-block declarations, declaration-after-statement
+bash scripts/check-info-version.sh # display_version matches Makefile CORE_BASE_VERSION
+ec # editorconfig-checker (install: brew install editorconfig-checker)
+cppcheck --enable=warning,performance,portability \
+ --suppressions-list=cppcheck-suppressions.txt \
+ -i src/m68000 -i src/bios -i libretro-common src/ libretro.c
+clang-format-diff -p1 -style=file < <(git diff -U0 develop...HEAD) # advisory; format check on changed lines
+```
+
+### Pre-commit hook
+
+`scripts/install-hooks.sh` installs a one-line pre-commit that runs `c89-lint.sh` against staged `.c` files. Run it once after cloning:
+
+```bash
+bash scripts/install-hooks.sh
+```
+
+## Commit style
+
+- Imperative present (`fix bug`, not `fixed bug`).
+- Subject ≤ 72 chars; body wrap ~72.
+- Prefix tags help skim `git log`: `ci:`, `build:`, `fix:`, `feat:`, `docs:`, `test:`, `refactor:`, `perf:`.
+- Reference issues / PRs in the body, not the subject.
+
+## C89 / GNU89 strict
+
+The libretro buildbot uses MSVC on Windows — MSVC C89 mode is strict about declarations-before-statements and forbids C99 features. See [`CLAUDE.md`](CLAUDE.md) for the full rule set.
+
+## Releases
+
+See [`docs/release-process.md`](docs/release-process.md) for the full release flow (cut `release/X.Y.Z` from `develop`, tag on `master`, back-merge to `develop`).
diff --git a/Makefile b/Makefile
index 670967b7..086e3db6 100644
--- a/Makefile
+++ b/Makefile
@@ -43,30 +43,39 @@ else ifneq ($(findstring MINGW,$(shell uname -a)),)
endif
TARGET_NAME := virtualjaguar
-GIT_VERSION := " $(shell git rev-parse --short HEAD || echo unknown)"
-ifneq ($(GIT_VERSION)," unknown")
- CFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\"
-endif
+
+# Single source-of-truth for the human-readable version string.
+# Bumped by .github/workflows/version-bump.yml (greps this line).
+# Composed into CORE_VERSION in src/core/version.h, generated below.
+CORE_BASE_VERSION := v2.2.0
+
ifeq ($(DEBUG),1)
-BUILD_TIMESTAMP := " debug $(shell date -u +%Y-%m-%dT%H:%M:%SZ)"
- CFLAGS += -DBUILD_TIMESTAMP=\"$(BUILD_TIMESTAMP)\"
+ CFLAGS += -DBUILD_TIMESTAMP="\"debug $(shell date -u +%Y-%m-%dT%H:%M:%SZ)\""
endif
-# GNU-ld --version-script choice.
-# link.T : production ABI (retro_* only).
-# link-test.T : wide symbol set used by white-box test harnesses.
-# The `test` target re-invokes make with TEST_EXPORTS=1 so the
-# shipped .so on `make` (default) hides internal symbols, while
-# `make test` produces a .so the test binaries can dlsym into.
-# Only effective on platforms that link with GNU ld --version-script
-# (Linux, Windows MSYS2, ARM, etc.); macOS / iOS / tvOS dylibs and
-# static archives ignore this and currently still export everything
-# with default visibility.
+# Symbol export gating.
+#
+# GNU ld (Linux, Windows MSYS2, ARM, ...) honours --version-script:
+# link.T : production ABI (retro_* only).
+# link-test.T : wide symbol set used by white-box test harnesses.
+#
+# Mach-O ld64 (macOS / iOS / tvOS) ignores --version-script; it uses
+# -exported_symbols_list instead:
+# exports.list : production retro_* only.
+# exports-test.list : wide test ABI (mirrors link-test.T).
+#
+# The `test` target re-invokes make with TEST_EXPORTS=1 so the shipped
+# library on default `make` hides internals, while `make test` produces
+# a library the test binaries can dlsym into. Static archives ignore
+# both mechanisms and still export everything with default visibility.
ifeq ($(TEST_EXPORTS),1)
LINK_SCRIPT := link-test.T
+MACHO_EXPORTS := exports-test.list
else
LINK_SCRIPT := link.T
+MACHO_EXPORTS := exports.list
endif
+MACHO_EXPORTS_FLAGS := -Wl,-exported_symbols_list,$(MACHO_EXPORTS)
# Unix
ifeq ($(platform), unix)
@@ -82,8 +91,8 @@ ifeq ($(platform), unix)
# Platform affix = classic__<µARCH>
# Help at https://modmyclassic.com/comp
-# (armv7 a7, hard point, neon based) ###
-# NESC, SNESC, C64 mini
+# (armv7 a7, hard point, neon based) ###
+# NESC, SNESC, C64 mini
else ifeq ($(platform), classic_armv7_a7)
TARGET := $(TARGET_NAME)_libretro.so
fpic := -fPIC
@@ -108,13 +117,13 @@ else ifeq ($(platform), classic_armv7_a7)
LDFLAGS += -static-libgcc -static-libstdc++
endif
endif
-#######################################
-
+#######################################
+
# OSX
else ifeq ($(platform), osx)
TARGET := $(TARGET_NAME)_libretro.dylib
fpic := -fPIC
- SHARED := -dynamiclib
+ SHARED := -dynamiclib $(MACHO_EXPORTS_FLAGS)
ifeq ($(arch),ppc)
FLAGS += -DMSB_FIRST
OLD_GCC = 1
@@ -141,7 +150,7 @@ endif
else ifneq (,$(findstring ios,$(platform)))
TARGET := $(TARGET_NAME)_libretro_ios.dylib
fpic := -fPIC
- SHARED := -dynamiclib
+ SHARED := -dynamiclib $(MACHO_EXPORTS_FLAGS)
MINVERSION :=
ifeq ($(IOSSDK),)
IOSSDK := $(shell xcodebuild -version -sdk iphoneos Path)
@@ -165,7 +174,7 @@ else ifeq ($(platform), tvos-arm64)
# tvOS
TARGET := $(TARGET_NAME)_libretro_tvos.dylib
fpic := -fPIC
- SHARED := -dynamiclib
+ SHARED := -dynamiclib $(MACHO_EXPORTS_FLAGS)
ifeq ($(IOSSDK),)
IOSSDK := $(shell xcodebuild -version -sdk appletvos Path)
endif
@@ -544,6 +553,36 @@ include Makefile.common
OBJECTS := $(SOURCES_CXX:.cpp=.o) $(SOURCES_C:.c=.o)
+# ----------------------------------------------------------------
+# version.h: generated header read by libretro.c. Single source of
+# truth is CORE_BASE_VERSION above; the script also stamps in the
+# short git rev.
+#
+# Regeneration runs at Makefile parse time via $(shell ...) so the
+# dependency graph sees a stable file with a stable mtime. The
+# alternative (a `: FORCE` rule) was racy under `make -j4` on the
+# stock /usr/bin/make 3.81 still shipped on macOS, which silently
+# stopped mid-build. The script does an in-place cmp + mv so
+# unchanged content leaves mtime untouched and incremental builds
+# stay incremental.
+# ----------------------------------------------------------------
+VERSION_H := $(CORE_DIR)/src/core/version.h
+
+# Skip the generator for read-only / metadata-only goals -- no point
+# spawning bash for `make clean`, `make print-FOO`, `make lint`, or
+# `make help`. Builds (the empty MAKECMDGOALS case, default target)
+# always run it.
+NO_GEN_GOALS := clean lint help print-%
+ifeq ($(filter-out $(NO_GEN_GOALS),$(or $(MAKECMDGOALS),all)),)
+# All requested goals are read-only -- skip generator.
+else
+_VERSION_GEN := $(shell bash scripts/gen-version-h.sh && echo ok)
+endif
+
+# Note: $(CORE_DIR)/libretro.o: $(VERSION_H) dependency is wired up
+# AFTER the `all:` rule below, so Make 3.81 doesn't latch onto
+# libretro.o as the default goal.
+
ifeq ($(DEBUG),1)
ifneq (,$(findstring msvc,$(platform)))
CFLAGS += -MTd
@@ -577,6 +616,16 @@ ifeq ($(RELEASE_DEBUG_INFO),1)
endif
endif
+# COVERAGE=1 instruments the build with gcov. Used by `make coverage`
+# below; don't combine with optimized builds. Compiler emits .gcno
+# files at build time, .gcda files at run time.
+ifeq ($(COVERAGE),1)
+ ifeq (,$(findstring msvc,$(platform)))
+ FLAGS += --coverage -O0 -g
+ LDFLAGS += --coverage
+ endif
+endif
+
ifeq (,$(findstring msvc,$(platform)))
FLAGS += -ffast-math -fomit-frame-pointer -fno-common
endif
@@ -624,7 +673,7 @@ CFLAGS += -DDEBUG_PRESENTATION
endif
OBJOUT = -o
-LINKOUT = -o
+LINKOUT = -o
ifneq (,$(findstring msvc,$(platform)))
OBJOUT = -Fo
@@ -655,6 +704,10 @@ else
$(LD) $(LINKOUT)$@ $^ $(LDFLAGS)
endif
+# version.h dependency hook (must come after `all:` so Make 3.81 on
+# stock macOS doesn't latch onto libretro.o as the default goal).
+$(CORE_DIR)/libretro.o: $(VERSION_H)
+
clean:
rm -f $(TARGET) $(OBJECTS) \
test/test_cheat test/test_event_queue test/test_blitter_simd \
@@ -788,12 +841,46 @@ test/tools/test_memory_map: test/tools/test_memory_map.c
-o $@ test/tools/test_memory_map.c -ldl
endif
-.PHONY: clean test lint
+.PHONY: clean test lint coverage benchmark
endif
lint:
@scripts/c89-lint.sh
+# `make coverage` -- builds with gcov instrumentation, runs the full
+# test suite, and produces a Cobertura XML report at coverage.xml plus
+# a textual summary. See gcovr.cfg for path filters.
+coverage:
+ $(MAKE) clean
+ $(MAKE) COVERAGE=1 TEST_EXPORTS=1 -j$(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
+ $(MAKE) COVERAGE=1 TEST_EXPORTS=1 test
+ gcovr --config gcovr.cfg --xml-pretty -o coverage.xml --txt --print-summary
+
+# `make benchmark` -- headless wall-clock perf measurement on a fixed
+# ROM. Boots the core via dlopen, runs $(BENCH_FRAMES) frames after
+# $(BENCH_WARMUP) warmup, prints FPS / ms-per-frame. Use during
+# perf-tuning code changes; commit-by-commit deltas are the signal.
+#
+# Override on the command line:
+# make benchmark BENCH_ROM=test/roms/private/Atari\ Karts.jag
+# make benchmark BENCH_FRAMES=3000 BENCH_WARMUP=120
+# make benchmark BENCH_BLITTER=accurate # default: fast
+BENCH_ROM ?= test/roms/yarc.j64
+BENCH_FRAMES ?= 600
+BENCH_WARMUP ?= 60
+BENCH_BLITTER ?= fast
+benchmark: $(TARGET)
+ @# Build the harness inline so this works whether or not TEST_EXPORTS=1
+ @# was used for $(TARGET); the harness only uses retro_* exports.
+ @# -ldl is Linux-specific; macOS/BSD provide dl* in libSystem/libc
+ @# (and Apple's clang silently accepts -ldl as a no-op, but other
+ @# linkers may not).
+ $(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
+ -o test/tools/test_benchmark test/tools/test_benchmark.c \
+ $(if $(filter Linux,$(shell uname -s)),-ldl)
+ ./test/tools/test_benchmark ./$(TARGET) $(BENCH_ROM) $(BENCH_FRAMES) \
+ --warmup $(BENCH_WARMUP) --blitter $(BENCH_BLITTER)
+
print-%:
@echo '$*=$($*)'
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..88411bfc
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,26 @@
+# Codecov config: per-PR coverage delta + project floor.
+#
+# Patch threshold = 0% means any drop in patch coverage flags red.
+# Project threshold = 1% absorbs measurement noise from re-runs of
+# nondeterministic tests.
+
+coverage:
+ status:
+ project:
+ default:
+ threshold: 1%
+ patch:
+ default:
+ threshold: 0%
+
+ignore:
+ - "libretro-common/**"
+ - "src/m68000/**"
+ - "src/bios/jag*.c"
+ - "test/**"
+ - "src/core/version.h"
+
+comment:
+ layout: "reach, diff, flags, files"
+ behavior: default
+ require_changes: false
diff --git a/cppcheck-suppressions.txt b/cppcheck-suppressions.txt
new file mode 100644
index 00000000..4c503d2a
--- /dev/null
+++ b/cppcheck-suppressions.txt
@@ -0,0 +1,38 @@
+# cppcheck suppressions for the libretro CI job.
+#
+# Each suppression is one of two shapes:
+# : - suppress all findings in
+# :: - suppress one specific occurrence
+#
+# RULE: every entry needs a one-line comment. If you find yourself
+# adding a suppression for a new issue, that's a hint the issue should
+# probably be fixed instead.
+
+# DSP / GPU RAM bounds: cppcheck doesn't model that GET16/GET32 macros
+# read from a 2-byte / 4-byte sliding window inside the dsp_ram_8 /
+# gpu_ram_8 backing arrays, and that the bounds check `offset <=
+# BASE + (size - 1)` is one short for the 2-byte / 4-byte reads.
+# These read past the declared array size by 1-3 bytes -- in practice
+# the next byte is part of an adjacent register / pad and is unused,
+# but it is technically OOB and worth a follow-up audit.
+arrayIndexOutOfBoundsCond:src/jerry/dsp.c
+arrayIndexOutOfBoundsCond:src/tom/gpu.c
+
+# Blitter long-shift code paths are guarded by a runtime condition
+# (cppcheck cites the guard explicitly in the "See condition at line
+# N" note). Safe under the existing branch.
+shiftTooManyBits:src/tom/blitter.c
+
+# Pre-existing signed-int shift in the GPU shifter unit -- standard
+# C UB (shifting a signed 32-bit value by 31 lands in the sign bit).
+# Tracked for follow-up.
+shiftTooManyBitsSigned:src/tom/gpu.c
+
+# Object Processor sign-extension idiom: cast to int8_t then >> 4 to
+# sign-extend a 4-bit nibble. Implementation-defined under C89 but
+# every compiler we target does arithmetic shift on signed.
+shiftNegativeLHS:src/tom/op.c
+
+# libretro-common conditional includes that depend on host-platform
+# macros cppcheck doesn't see (e.g. _WIN32, __APPLE__).
+preprocessorErrorDirective:libretro-common/include/retro_common_api.h
diff --git a/docs/profiling.md b/docs/profiling.md
new file mode 100644
index 00000000..5c2dbd20
--- /dev/null
+++ b/docs/profiling.md
@@ -0,0 +1,123 @@
+# Profiling guide
+
+How to measure where the Virtual Jaguar libretro core actually spends time.
+
+## TL;DR — `make benchmark`
+
+Wall-clock baseline you can run on every commit:
+
+```bash
+make benchmark # default: yarc.j64, 600 frames, fast blitter
+make benchmark BENCH_FRAMES=3000 # longer run (smoother numbers)
+make benchmark BENCH_BLITTER=accurate # A/B against the slow path
+make benchmark BENCH_ROM=test/roms/private/Atari\ Karts.jag
+```
+
+Reports `Frames/sec`, `Time/frame`, total wall time. Boots the core via `dlopen`, runs N frames headless (no video presentation, no audio output), so you measure pure emulator work.
+
+**Use it as a delta**: capture baseline before your change, run again after. Don't trust absolute numbers across hosts (CPU, thermals, scheduler, big.LITTLE pinning). Do trust same-host commit-to-commit deltas.
+
+> The harness lives at `test/tools/test_benchmark.c`. Read it if you want to measure something specific (per-subsystem timing, only-DSP, etc.) — it's <400 lines.
+
+## macOS — Instruments / `sample`
+
+**Instruments (Time Profiler)** is the easiest way to get a flame graph on macOS.
+
+```bash
+make benchmark BENCH_FRAMES=6000 BENCH_WARMUP=120 &
+BENCH_PID=$!
+
+# Sample for 30 seconds, output to .trace bundle
+xcrun xctrace record --template "Time Profiler" --attach $BENCH_PID --output bench.trace --time-limit 30s
+open bench.trace
+```
+
+The default symbolication is good — you'll see `OPProcessFixedBitmap`, `BlitterMidsummer2`, `DSPExec` etc. as top hot frames if they're slow.
+
+For a quick text dump without the GUI:
+
+```bash
+sample $BENCH_PID 5 -file /tmp/sample.txt
+# 5-second sample. Read /tmp/sample.txt for collapsed call stacks.
+```
+
+## Linux — `perf` + flamegraph
+
+```bash
+sudo apt install -y linux-tools-common linux-tools-generic
+git clone https://github.com/brendangregg/FlameGraph /tmp/flamegraph
+
+make benchmark BENCH_FRAMES=6000 BENCH_WARMUP=120 &
+BENCH_PID=$!
+
+perf record -F 999 -g -p $BENCH_PID -- sleep 30
+perf script | /tmp/flamegraph/stackcollapse-perf.pl | /tmp/flamegraph/flamegraph.pl > flame.svg
+open flame.svg
+```
+
+`-F 999` = 999 Hz sample rate (avoid 1000 Hz lockstep aliasing with display refresh). `-g` = capture call graphs.
+
+## Hot paths to know
+
+Suspicious-by-default places when something gets slow:
+
+| Subsystem | File | Notes |
+|---|---|---|
+| Object Processor (sprites, bitmaps) | `src/tom/op.c` | `OPProcessFixedBitmap`, `OPProcessScaledBitmap`, `OPDiscoverObjects`. Dominant on heavy-OP scenes (Wolf3D, Tempest 2000). |
+| Blitter | `src/tom/blitter.c`, `blitter_mmio.c`, `blitter_simd_*.c` | Two paths: fast (`blitter_generic`, the upstream-derived path) and accurate (`BlitterMidsummer2`). SIMD (`blitter_simd_{sse2,neon,scalar}.c`) is currently wired only into the **accurate** blitter's pixel kernel — see [issue #124](https://github.com/libretro/virtualjaguar-libretro/issues/124) for the plan to widen SIMD coverage. |
+| 68K | `src/m68000/cpuemu.c` | Machine-generated UAE. ~1.8 MB of source. If this is hot, there's not much to do beyond JIT (out of scope). |
+| GPU (RISC, 26.6 MHz) | `src/tom/gpu.c` | `GPUExec` per-instruction. Hot when game uses GPU heavily (most do). |
+| DSP (RISC, audio) | `src/jerry/dsp.c` | `DSPExec` per-instruction. See `src/jerry/dsp_acc40.h` for the 40-bit MAC. |
+| Frame loop | `src/core/jaguar.c` | `JaguarExecuteNew` is the event-driven driver. If the event queue is hot, look at `src/core/event.c`. |
+
+## SIMD A/B testing
+
+The blitter has three implementations, selected at build time via `BLITTER_SIMD`:
+
+```bash
+make BLITTER_SIMD=neon -j4 && make benchmark # ARM
+make BLITTER_SIMD=sse2 -j4 && make benchmark # x86_64
+make BLITTER_SIMD=scalar -j4 && make benchmark # portable fallback
+```
+
+Auto-detection picks NEON on aarch64 / SSE2 on x86_64 / scalar elsewhere (see `Makefile.common`). Force the scalar build to verify SIMD is actually winning — when the gap closes, your bottleneck moved elsewhere.
+
+## Build flavors
+
+| Goal | Flags |
+|---|---|
+| Production perf | `make` (default; `-O2 -DNDEBUG -ffast-math -fomit-frame-pointer`) |
+| Profiling (good symbols, near-prod perf) | `make RELEASE_DEBUG_INFO=1` (`-O2 -g`). Strips later if shipping. |
+| Sanitizers | `make CC="clang -fsanitize=address,undefined -O1 -g"` (catches bugs, halves perf) |
+| Coverage | `make COVERAGE=1` (`-O0 -g --coverage`). Don't profile this — coverage instrumentation overhead is ~3× and not representative. |
+| Debug stepping | `make DEBUG=1` (`-O0 -g`) |
+
+## Cycle / instruction counts
+
+The core is **event-driven, not cycle-accurate** (`docs/source-layout.md` covers the rationale). `JaguarExecuteNew` runs the 68K to the next event, then GPU, then fires callbacks. Don't expect cycle-counter results to match real hardware — measure wall-clock instead.
+
+If you do need cycle-level inspection:
+
+```bash
+# Linux: cycles + instructions per loop iteration
+perf stat -e cycles,instructions,branches,branch-misses ./test/tools/test_benchmark ./virtualjaguar_libretro.so test/roms/yarc.j64 600
+
+# macOS: Instruments has a "CPU Counters" template
+xcrun xctrace record --template "CPU Counters" --launch -- ./test/tools/test_benchmark ...
+```
+
+## Regression triage
+
+If `make benchmark` shows a regression after your change:
+
+1. **Run twice** — check for noise. Same-host runs typically vary <2%. >5% delta is real signal.
+2. **Bisect by commit**: `git bisect start HEAD `, mark good/bad with `make benchmark` results until you isolate the offender.
+3. **Check both blitters** (`BENCH_BLITTER=fast` vs `accurate`) — sometimes a regression only shows on one path.
+4. **Profile, don't guess** — the bottleneck is rarely where you'd expect on this codebase.
+
+## See also
+
+- [`CLAUDE.md`](../CLAUDE.md) — hardware model + repo layout
+- [`docs/source-layout.md`](source-layout.md) — file-by-file source tour
+- [`docs/emulation-bug-hunt-todos.md`](emulation-bug-hunt-todos.md) — known performance / accuracy follow-ups
+- [`test/tools/test_benchmark.c`](../test/tools/test_benchmark.c) — the harness itself
diff --git a/docs/release-process.md b/docs/release-process.md
index 5e00ce57..365d9317 100644
--- a/docs/release-process.md
+++ b/docs/release-process.md
@@ -2,12 +2,26 @@
How to ship a new tagged release of the libretro core.
+## Branching model (GitFlow)
+
+This repo uses a [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/)-style layout:
+
+- `master` — release-only. Tagged commits, hotfix merges, release-branch merges. Default branch on GitHub for visibility / clone defaults.
+- `develop` — integration branch. All feature work flows in here first.
+- `feature/*` — branched from `develop`, merged back via PR.
+- `release/X.Y.Z` — branched from `develop` when cutting a release; bug-fix-only until tagged on `master`, then back-merged to `develop`.
+- `hotfix/X.Y.Z` — branched from `master`, fixed, tagged on `master`, back-merged to `develop`.
+
+PRs targeting `master` directly trigger a friendly comment from `.github/workflows/warn-pr-base.yml` asking the contributor to retarget to `develop` (skipped for `release/*` and `hotfix/*` source branches).
+
## TL;DR
-1. Merge the release PR into `master`.
-2. `git tag vX.Y.Z && git push libretro vX.Y.Z` (or via GitHub UI).
-3. Watch [Actions](https://github.com/libretro/virtualjaguar-libretro/actions) — `release.yml` builds 14 platforms, generates `SHA256SUMS.txt`, and publishes the release with `docs/RELEASE_NOTES_vX.Y.Z.md` as the body.
-4. After the tag publishes, send a small PR to [libretro/libretro-super](https://github.com/libretro/libretro-super) updating `dist/info/virtualjaguar_libretro.info` to match `dist/info/virtualjaguar_libretro.info` from this repo at the tag.
+1. Cut `release/X.Y.Z` from `develop`. Bump `CORE_BASE_VERSION` in `Makefile` (or use the `Bump Version & Release` workflow). Update `docs/RELEASE_NOTES_vX.Y.Z.md`. Open a PR from `release/X.Y.Z` → `master`.
+2. Merge into `master`.
+3. `git tag vX.Y.Z && git push libretro vX.Y.Z` (or via GitHub UI).
+4. Watch [Actions](https://github.com/libretro/virtualjaguar-libretro/actions) — `release.yml` builds 14 platforms, generates `SHA256SUMS.txt`, and publishes the release with `docs/RELEASE_NOTES_vX.Y.Z.md` as the body.
+5. **Back-merge `master` → `develop`** so the version bump and any release-branch fixes are in `develop`: `git checkout develop && git merge master && git push libretro develop`.
+6. After the tag publishes, send a small PR to [libretro/libretro-super](https://github.com/libretro/libretro-super) updating `dist/info/virtualjaguar_libretro.info` to match `dist/info/virtualjaguar_libretro.info` from this repo at the tag.
## Detail
@@ -75,6 +89,22 @@ The `docs/RELEASE_NOTES_v.md` file is written by hand (or with a sub-agent'
For future releases (where there's a previous tag to diff against), use `git shortlog vPREV..HEAD` instead of `libretro/master..HEAD`.
+### Hotfix flow
+
+When a critical bug ships in a release and can't wait for the next `develop` cycle:
+
+```
+git checkout master && git pull
+git checkout -b hotfix/X.Y.Z+1
+# ... fix, bump CORE_BASE_VERSION patch, update RELEASE_NOTES, commit ...
+gh pr create --base master --title "hotfix: " --body "Fixes #N. Bumps to vX.Y.Z+1."
+# After merge:
+git tag vX.Y.Z+1 && git push libretro vX.Y.Z+1
+git checkout develop && git pull && git merge master && git push libretro develop
+```
+
+The `hotfix/*` source branch suppresses the warn-on-master-PR workflow.
+
### 6. If a release-job step fails
The workflow runs only on `v*` tag push, but the `release` job is re-runnable from the [Actions tab](https://github.com/libretro/virtualjaguar-libretro/actions) without needing a new tag. Common failure modes:
diff --git a/exports-test.list b/exports-test.list
new file mode 100644
index 00000000..0b3fbff2
--- /dev/null
+++ b/exports-test.list
@@ -0,0 +1,35 @@
+# Wide symbol export list for Mach-O test builds (TEST_EXPORTS=1).
+#
+# Mirrors link-test.T: exposes the internal symbols our headless test
+# harnesses dlsym into. Used only when TEST_EXPORTS=1 (set by the
+# `test` target); production .dylib uses exports.list (retro_* only).
+#
+# If you add a new symbol here, that's an extension to the test ABI
+# only -- it does NOT change the production-shipped .dylib.
+#
+# Symbol names are prefixed with `_` per the Mach-O ABI.
+_retro_*
+_DSP*
+_dsp_*
+_m68k_*
+_Jaguar*
+_jaguar*
+_Halfline*
+_jaguarMainRAM
+_jaguarMainROM
+_jagMemSpace
+_pcQueue
+_pcQPtr
+_a6Queue
+_d0Queue
+_GPU*
+_gpu_*
+_JERRY*
+_TOM*
+_OP*
+_tomRam8
+_regs
+_sclk
+_smode
+_lowerField
+_vjs
diff --git a/exports.list b/exports.list
new file mode 100644
index 00000000..a30d3048
--- /dev/null
+++ b/exports.list
@@ -0,0 +1,13 @@
+# Production ABI export list for Mach-O (macOS / iOS / tvOS).
+#
+# Mirrors link.T: only the libretro retro_* entry points are exported
+# from the shipped .dylib. Internal emulator state (DSP, GPU, blitter,
+# 68K registers, jaguarMainRAM, etc.) stays hidden so frontends and
+# future refactors don't accidentally pin themselves to internals.
+#
+# White-box test harnesses need the wider symbol set; the Makefile
+# switches to exports-test.list when TEST_EXPORTS=1 (which the `test`
+# target sets automatically).
+#
+# Symbol names are prefixed with `_` per the Mach-O ABI.
+_retro_*
diff --git a/gcovr.cfg b/gcovr.cfg
new file mode 100644
index 00000000..77e70922
--- /dev/null
+++ b/gcovr.cfg
@@ -0,0 +1,17 @@
+# gcovr config used by `make coverage` and the CI coverage job.
+#
+# Filters to in-tree code only. The vendored libretro-common subtree,
+# the machine-generated UAE 68K core (src/m68000), and the bin2c hex
+# tables under src/bios/jag*.c are excluded -- coverage of those is
+# uninteresting and would dominate the report.
+
+filter = src/
+
+exclude = libretro-common/.*
+exclude = src/m68000/.*
+exclude = src/bios/jag.*\.c
+exclude = test/.*
+
+exclude-throw-branches = yes
+exclude-unreachable-branches = yes
+print-summary = yes
diff --git a/jni/Android.mk b/jni/Android.mk
index cfc3b966..47159f6b 100644
--- a/jni/Android.mk
+++ b/jni/Android.mk
@@ -6,10 +6,10 @@ include $(CORE_DIR)/Makefile.common
COREFLAGS := -DINLINE="inline" -D__LIBRETRO__ $(INCFLAGS)
-GIT_VERSION := " $(shell git rev-parse --short HEAD || echo unknown)"
-ifneq ($(GIT_VERSION)," unknown")
- COREFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\"
-endif
+# libretro.c includes the generated src/core/version.h. Generate it
+# at parse time -- ndk-build doesn't go through the project Makefile,
+# so the parse-time $(shell ...) there doesn't fire for us.
+_VERSION_GEN := $(shell sh $(CORE_DIR)/scripts/gen-version-h.sh && echo ok)
include $(CLEAR_VARS)
LOCAL_MODULE := retro
diff --git a/libretro.c b/libretro.c
index b6c920c8..28b1a05c 100644
--- a/libretro.c
+++ b/libretro.c
@@ -19,6 +19,7 @@
#include "tom.h"
#include "state.h"
#include "log.h"
+#include "version.h" /* generated; defines CORE_VERSION */
#define SAMPLERATE 48000
#define BUFPAL 1920
@@ -38,15 +39,6 @@
* lands on a future PR. */
#define JAGUAR_VALID_EXTENSIONS "j64|jag|rom|abs|cof|bin|prg"
-#ifndef GIT_VERSION
-#define GIT_VERSION ""
-#endif
-#ifdef BUILD_TIMESTAMP
-#define CORE_VERSION "v2.2.0" GIT_VERSION BUILD_TIMESTAMP
-#else
-#define CORE_VERSION "v2.2.0" GIT_VERSION
-#endif
-
int videoWidth = 0;
int videoHeight = 0;
uint32_t *videoBuffer = NULL;
diff --git a/scripts/c89-lint.sh b/scripts/c89-lint.sh
index 3001622f..f543cf1c 100755
--- a/scripts/c89-lint.sh
+++ b/scripts/c89-lint.sh
@@ -8,6 +8,12 @@
set -e
+# libretro.c includes the generated src/core/version.h. Make sure it
+# exists before we run -fsyntax-only -- this script is invoked from CI
+# and pre-commit hooks where `make` may not have run yet.
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+[ -f "$ROOT/src/core/version.h" ] || sh "$ROOT/scripts/gen-version-h.sh"
+
CC="${CC:-gcc}"
CFLAGS="-fsyntax-only -std=gnu89 -Werror=declaration-after-statement"
INCLUDES="-I. -Isrc -Isrc/core -Isrc/tom -Isrc/jerry -Isrc/cd -Isrc/bios -Isrc/m68000 -Ilibretro-common/include"
diff --git a/scripts/check-info-version.sh b/scripts/check-info-version.sh
new file mode 100755
index 00000000..9cd50b40
--- /dev/null
+++ b/scripts/check-info-version.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# Verify dist/info/virtualjaguar_libretro.info `display_version` matches
+# the Makefile's CORE_BASE_VERSION. Run on every CI build so a release
+# can't ship with a stale .info field that would mislead RetroArch's
+# "core version" UI.
+
+set -e
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+INFO="$ROOT/dist/info/virtualjaguar_libretro.info"
+MAKEFILE="$ROOT/Makefile"
+
+if [ ! -f "$INFO" ]; then
+ echo "::error::missing $INFO"
+ exit 1
+fi
+
+# Portable across BSD/GNU sed.
+MAKE_VER=$(sed -n 's/^CORE_BASE_VERSION[[:space:]]*:*=[[:space:]]*\(.*\)/\1/p' "$MAKEFILE" | head -1)
+INFO_VER=$(sed -n 's/^display_version[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' "$INFO" | head -1)
+
+if [ -z "$MAKE_VER" ]; then
+ echo "::error::could not parse CORE_BASE_VERSION from $MAKEFILE"
+ exit 1
+fi
+if [ -z "$INFO_VER" ]; then
+ echo "::error::could not parse display_version from $INFO"
+ exit 1
+fi
+
+if [ "$MAKE_VER" != "$INFO_VER" ]; then
+ echo "::error::version mismatch: Makefile CORE_BASE_VERSION=$MAKE_VER, .info display_version=$INFO_VER"
+ echo "Update dist/info/virtualjaguar_libretro.info \`display_version\` to match before tagging."
+ exit 1
+fi
+
+echo "OK: CORE_BASE_VERSION = display_version = $MAKE_VER"
diff --git a/scripts/gen-version-h.sh b/scripts/gen-version-h.sh
new file mode 100755
index 00000000..e0432e9a
--- /dev/null
+++ b/scripts/gen-version-h.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env sh
+# Generates src/core/version.h from CORE_BASE_VERSION pinned in
+# Makefile + the current short git rev. Called by:
+# - The project Makefile at parse time via $(shell ...) (gated to
+# skip read-only goals like clean / print-* / lint).
+# - jni/Android.mk via $(shell ...) so ndk-build picks it up.
+# - scripts/c89-lint.sh, scripts/install-hooks.sh's pre-commit, and
+# the c-cpp.yml msvc-check step (which invokes cl.exe directly).
+#
+# Output is gitignored. Re-runs are content-aware: if the new
+# contents match the existing file, the mtime is left alone so
+# incremental builds don't needlessly rebuild libretro.o.
+#
+# POSIX sh compatible -- no bashisms.
+
+set -e
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+OUT="$ROOT/src/core/version.h"
+
+# Portable sed extraction that works on both GNU and BSD sed.
+CORE_BASE_VERSION=$(sed -n 's/^CORE_BASE_VERSION[[:space:]]*:*=[[:space:]]*\(.*\)/\1/p' "$ROOT/Makefile" | head -1)
+if [ -z "$CORE_BASE_VERSION" ]; then
+ echo "gen-version-h.sh: failed to read CORE_BASE_VERSION from Makefile" >&2
+ exit 1
+fi
+
+GIT_REV=$(cd "$ROOT" && git rev-parse --short HEAD 2>/dev/null || echo unknown)
+
+mkdir -p "$(dirname "$OUT")"
+TMP="$OUT.tmp"
+cat > "$TMP" << EOF
+/* Auto-generated by scripts/gen-version-h.sh. Do not edit; do not commit. */
+#ifndef VJAG_VERSION_H
+#define VJAG_VERSION_H
+#define CORE_BASE_VERSION "${CORE_BASE_VERSION}"
+#define GIT_VERSION " ${GIT_REV}"
+#ifdef BUILD_TIMESTAMP
+#define CORE_VERSION CORE_BASE_VERSION GIT_VERSION " " BUILD_TIMESTAMP
+#else
+#define CORE_VERSION CORE_BASE_VERSION GIT_VERSION
+#endif
+#endif
+EOF
+
+if cmp -s "$TMP" "$OUT" 2>/dev/null; then
+ rm "$TMP"
+else
+ mv "$TMP" "$OUT"
+fi
diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh
index 0222571f..33c24b06 100755
--- a/scripts/install-hooks.sh
+++ b/scripts/install-hooks.sh
@@ -1,13 +1,35 @@
#!/bin/sh
-# Install git hooks for this repository
+# Install git hooks for this repository.
+#
+# Currently installs a pre-commit that runs:
+# - scripts/c89-lint.sh on staged .c files (catches MSVC C89 violations)
+# - scripts/check-info-version.sh if anything under dist/info/ or
+# Makefile is staged (verifies display_version stays in sync)
+#
+# Skip with `git commit --no-verify` if you really need to (e.g., a WIP
+# squash); CI will catch it later anyway.
+
+set -e
+
HOOK_DIR="$(git rev-parse --git-dir)/hooks"
cat > "$HOOK_DIR/pre-commit" << 'HOOK'
#!/bin/sh
-STAGED_C=$(git diff --cached --name-only --diff-filter=ACM | grep '\.c$' || true)
-if [ -z "$STAGED_C" ]; then exit 0; fi
-exec scripts/c89-lint.sh $STAGED_C
+set -e
+
+STAGED=$(git diff --cached --name-only --diff-filter=ACM)
+
+# C89 lint on staged .c files
+STAGED_C=$(echo "$STAGED" | grep '\.c$' || true)
+if [ -n "$STAGED_C" ]; then
+ scripts/c89-lint.sh $STAGED_C
+fi
+
+# .info / Makefile version sync check
+if echo "$STAGED" | grep -qE '^(dist/info/|Makefile$)'; then
+ scripts/check-info-version.sh
+fi
HOOK
chmod +x "$HOOK_DIR/pre-commit"
-echo "Installed pre-commit hook (C89 lint)"
+echo "Installed pre-commit hook (C89 lint + .info version check)"
diff --git a/src/cd/cdrom.c b/src/cd/cdrom.c
index aae000eb..0ca90b13 100644
--- a/src/cd/cdrom.c
+++ b/src/cd/cdrom.c
@@ -125,7 +125,7 @@
;
eeprom equ $DFFF2c ;interface to CD-eeprom
;
- ; bit3 - busy if 0 after write cmd, or Data In after read cmd
+ ; bit3 - busy if 0 after write cmd, or Data In after read cmd
; bit2 - Data Out
; bit1 - clock
; bit0 - Chip Select (CS)
@@ -204,8 +204,10 @@ void CDROMDone(void)
void BUTCHExec(uint32_t cycles)
{
#if 1
- // We're chickening out for now...
- return;
+ /* No-op for now: CD support is not exposed through this code path
+ * (HLE / DSP path handles audio). No `return` -- end of void
+ * function suffices, and clang-tidy flags an explicit `return;`
+ * here as redundant. */
#else
// extern uint8_t * jerry_ram_8; // Hmm.
diff --git a/src/jerry/jerry.h b/src/jerry/jerry.h
index 1fb67586..2d10739e 100644
--- a/src/jerry/jerry.h
+++ b/src/jerry/jerry.h
@@ -37,7 +37,7 @@ enum
IRQ2_TIMER1=0x04,
IRQ2_TIMER2=0x08,
IRQ2_ASI=0x10,
- IRQ2_SSI=0x20
+ IRQ2_SSI=0x20
};
bool JERRYIRQEnabled(int irq);
diff --git a/src/tom/blitter.c b/src/tom/blitter.c
index 01560b41..ac40d94e 100644
--- a/src/tom/blitter.c
+++ b/src/tom/blitter.c
@@ -1662,7 +1662,7 @@ A2ptrldi := NAN2 (a2ptrldi, a2update\, a2pldt);*/
else
inner_mask = 0;
- /* The actual mask used should be the
+ /* The actual mask used should be the
lesser of the window masks and
the inner mask, where is all cases 000 means 1000. */
window_mask = (window_mask == 0 ? 0x40 : window_mask);
diff --git a/src/tom/gpu.c b/src/tom/gpu.c
index 901ce397..077d8c55 100644
--- a/src/tom/gpu.c
+++ b/src/tom/gpu.c
@@ -338,7 +338,7 @@ uint32_t GPUReadLong(uint32_t offset, uint32_t who/*=UNKNOWN*/)
if (offset >= 0xF02000 && offset <= 0xF020FF)
{
uint32_t reg = (offset & 0xFC) >> 2;
- return (reg < 32 ? gpu_reg_bank_0[reg] : gpu_reg_bank_1[reg - 32]);
+ return (reg < 32 ? gpu_reg_bank_0[reg] : gpu_reg_bank_1[reg - 32]);
}
if ((offset >= GPU_WORK_RAM_BASE) && (offset <= GPU_WORK_RAM_BASE + 0x0FFC))
diff --git a/src/tom/op.c b/src/tom/op.c
index 80412127..727d80f1 100644
--- a/src/tom/op.c
+++ b/src/tom/op.c
@@ -535,7 +535,7 @@ void OPProcessFixedBitmap(uint64_t p0, uint64_t p1, bool render)
uint32_t lbufAddress;
uint8_t * currentLineBuffer;
int32_t startPos,endPos;
- // This is correct, the OP line buffer is a constant size...
+ // This is correct, the OP line buffer is a constant size...
int32_t limit = 720;
int32_t lbufWidth = 719;
uint32_t clippedWidth = 0, phraseClippedWidth = 0, dataClippedWidth = 0;//, phrasePixel = 0;
diff --git a/src/tom/op.h b/src/tom/op.h
index bd92092a..c72f3c17 100644
--- a/src/tom/op.h
+++ b/src/tom/op.h
@@ -19,7 +19,7 @@ void OPDone(void);
uint64_t OPLoadPhrase(uint32_t offset);
-void OPProcessList(int scanline, bool render);
+void OPProcessList(int halfline, bool render);
uint32_t OPGetListPointer(void);
void OPSetStatusRegister(uint32_t data);
uint32_t OPGetStatusRegister(void);
diff --git a/src/tom/tom.c b/src/tom/tom.c
index c776399f..542e54ad 100644
--- a/src/tom/tom.c
+++ b/src/tom/tom.c
@@ -307,7 +307,7 @@ uint8_t greencv[16][16] = {
{ 0, 19, 38, 57,77, 96, 115,134,154,173,192,211,231,250,255,255}, // E
{ 0, 17, 34, 51,68, 85, 102,119,136,153,170,187,204,221,238,255} // F
};
-
+
// Blue Color Values for CrY<->RGB Color Conversion
uint8_t bluecv[16][16] = {
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
@@ -455,7 +455,7 @@ void TOMFillLookupTables(void)
{
// NOTE: Jaguar 16-bit (non-CRY) color is RBG 556 like so:
// RRRR RBBB BBGG GGGG
-
+
unsigned i;
for(i=0; i<0x10000; i++)
RGB16ToRGB32[i] = 0xFF000000