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