diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index 7f52137c42..fcbd1f18e9 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -17,13 +17,13 @@ env: jobs: build: - continue-on-error: true strategy: + fail-fast: false matrix: arch: - - runner: ubuntu-22.04 + - runner: ubuntu-24.04 name: x86_64 - - runner: ubuntu-22.04-arm + - runner: ubuntu-24.04-arm name: aarch64 name: ${{ matrix.arch.name }} @@ -35,10 +35,13 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install --allow-downgrades cmake ninja-build extra-cmake-modules libpcap0.8-dev libsdl2-dev libenet-dev \ - qt6-{base,base-private,multimedia}-dev qt6-wayland libqt6svg6-dev libarchive-dev libzstd-dev libfuse2 libfaad-dev + sudo apt install --allow-downgrades g++-14 ninja-build extra-cmake-modules libpcap0.8-dev libsdl2-dev libenet-dev \ + qt6-{base,base-private,multimedia}-dev qt6-wayland libqt6svg6-dev libarchive-dev libzstd-dev libfuse2t64 libfaad-dev python3-venv + python3 -m venv "$RUNNER_TEMP/cmake-env" + "$RUNNER_TEMP/cmake-env/bin/pip" install cmake==3.31.6 + echo "$RUNNER_TEMP/cmake-env/bin" >> "$GITHUB_PATH" - name: Configure - run: cmake -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DMELONDS_EMBED_BUILD_INFO=ON + run: cmake -B build -G Ninja -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 -DCMAKE_INSTALL_PREFIX=/usr -DMELONDS_EMBED_BUILD_INFO=ON - name: Build run: | cmake --build build diff --git a/.github/workflows/core-regressions.yml b/.github/workflows/core-regressions.yml new file mode 100644 index 0000000000..4cb6fe4c73 --- /dev/null +++ b/.github/workflows/core-regressions.yml @@ -0,0 +1,79 @@ +name: Core regressions + +on: + push: + branches: + - master + - fix/core-regressions-20260907 + - modernize/cpp26-bitfield + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + regression: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - name: GCC 14 + compiler: g++-14 + flags: "" + - name: Clang 18 ASan UBSan + compiler: clang++-18 + flags: -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer + name: ${{ matrix.name }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Install C++26 toolchains + run: | + sudo apt-get update + sudo apt-get install -y g++-14 clang-18 + python3 -m venv "$RUNNER_TEMP/cmake-env" + "$RUNNER_TEMP/cmake-env/bin/pip" install cmake==3.31.6 + echo "$RUNNER_TEMP/cmake-env/bin" >> "$GITHUB_PATH" + - name: Configure + run: >- + cmake -S tests -B build-regression -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_CXX_COMPILER="${{ matrix.compiler }}" + -DCMAKE_CXX_FLAGS="${{ matrix.flags }}" + -DCMAKE_EXE_LINKER_FLAGS="${{ matrix.flags }}" + - name: Build + run: cmake --build build-regression --parallel 2 + - name: Test + run: ctest --test-dir build-regression --output-on-failure + + core-build: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + jit: ["ON", "OFF"] + name: C++26 core JIT ${{ matrix.jit }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Install toolchain + run: | + sudo apt-get update + sudo apt-get install -y g++-14 ninja-build + python3 -m venv "$RUNNER_TEMP/cmake-env" + "$RUNNER_TEMP/cmake-env/bin/pip" install cmake==3.31.6 + echo "$RUNNER_TEMP/cmake-env/bin" >> "$GITHUB_PATH" + - name: Configure full core + run: >- + cmake -S . -B build-core -G Ninja -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 + -DBUILD_QT_SDL=OFF -DENABLE_OGLRENDERER=OFF + -DENABLE_JIT=${{ matrix.jit }} + - name: Compile full core + run: cmake --build build-core --parallel 2 diff --git a/BUILD.md b/BUILD.md index 0ec9178278..b581b0e3e6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,5 +1,29 @@ # Building melonDS +## Toolchain requirements for this fork + +Use **CMake 3.30 or newer** and a compiler supporting **C++26 mode** (GCC 14+ +or Clang 17+). The build does not silently fall back to C++17/20/23. Language +mode support is not a promise that all C++26 features are available; see +[the migration notes](docs/Cpp26.md) for scope and verification boundaries. +Vendored libraries keep their own language requirements. + +For Ubuntu 24.04, install `g++-14 python3-venv`, install CMake 3.31.6 in a +virtual environment, activate it, and select `-DCMAKE_C_COMPILER=gcc-14 +-DCMAKE_CXX_COMPILER=g++-14` when configuring. For example: + +```sh +python3 -m venv .venv-build +. .venv-build/bin/activate +python -m pip install cmake==3.31.6 +cmake -B build -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 +``` + +The dependency lists below do not upgrade an old system compiler. Older +Ubuntu/BSD/Nix environments may need a newer toolchain separately. Linux CI +now builds on Ubuntu 24.04; do not assume its binaries retain Ubuntu 22.04 +runtime compatibility. Use a new build directory when switching compilers. + * [Linux](#linux) * [Windows](#windows) * [macOS](#macos) @@ -9,17 +33,17 @@ * Ubuntu: * All versions: `sudo apt install cmake extra-cmake-modules libcurl4-gnutls-dev libpcap0.8-dev libsdl2-dev libarchive-dev libenet-dev libzstd-dev libfaad-dev` * 24.04: `sudo apt install qt6-{base,base-private,multimedia,svg}-dev` - * 22.04: `sudo apt install qtbase6-dev qtbase6-private-dev qtmultimedia6-dev libqt6svg6-dev` + * 22.04: `sudo apt install qt6-base-dev qt6-base-private-dev qt6-multimedia-dev libqt6svg6-dev` * Older versions: `sudo apt install qtbase5-dev qtbase5-private-dev qtmultimedia5-dev libqt5svg5-dev` Also add `-DUSE_QT6=OFF` to the first CMake command below. * Fedora: `sudo dnf install gcc-c++ cmake extra-cmake-modules SDL2-devel libarchive-devel enet-devel libzstd-devel faad2-devel qt6-{qtbase,qtbase-private,qtmultimedia,qtsvg}-devel wayland-devel` * Arch Linux: `sudo pacman -S base-devel cmake extra-cmake-modules git libpcap sdl2 qt6-{base,multimedia,svg} libarchive enet zstd faad2` 2. Download the melonDS repository and prepare: ```bash - git clone https://github.com/melonDS-emu/melonDS + git clone https://github.com/gajae1/melonDS cd melonDS ``` -3. Compile: +3. Compile (select the newer compiler above where necessary): ```bash cmake -B build cmake --build build -j$(nproc --all) @@ -34,7 +58,7 @@ 4. Install git and clone the repository ```bash pacman -S git - git clone https://github.com/melonDS-emu/melonDS + git clone https://github.com/gajae1/melonDS cd melonDS ``` 5. Install dependencies: @@ -44,7 +68,7 @@ ``` 6. Install Qt and configure the build directory * Dynamic builds (with DLLs) - 1. Install Qt: `pacman -S -{qt6-base,qt6-svg,qt6-multimedia,qt6-svg,qt6-tools}` + 1. Install Qt: `pacman -S -{qt6-base,qt6-svg,qt6-multimedia,qt6-tools}` 2. Set up the build directory with `cmake -B build` * Static builds (without DLLs, standalone executable) 1. Install Qt: `pacman -S -qt5-static` @@ -59,7 +83,7 @@ If everything went well, melonDS should now be in the `build` folder. For dynami 2. Install dependencies: `brew install git pkg-config cmake sdl2 qt@6 libarchive enet zstd faad2` 3. Download the melonDS repository and prepare: ```zsh - git clone https://github.com/melonDS-emu/melonDS + git clone https://github.com/gajae1/melonDS cd melonDS ``` 4. Compile: @@ -77,5 +101,5 @@ If you want an app bundle that can be distributed to other computers without nee melonDS provides a Nix flake with support for both macOS and Linux. The [Nix package manager](https://nixos.org) needs to be installed to use it. -* To run melonDS, just type `nix run github:melonDS-emu/melonDS`. +* To run melonDS, just type `nix run github:gajae1/melonDS`. * To get a shell for development, clone the melonDS repository and type `nix develop` in its directory. diff --git a/CMakeLists.txt b/CMakeLists.txt index c74f6b93a7..aa883a3bd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.30) cmake_policy(VERSION 3.15) if (POLICY CMP0076) @@ -22,6 +22,10 @@ project(melonDS HOMEPAGE_URL "https://melonds.kuribo64.net" LANGUAGES C CXX) +if (NOT "cxx_std_26" IN_LIST CMAKE_CXX_COMPILE_FEATURES) + message(FATAL_ERROR "melonDS requires a compiler with C++26 mode (GCC 14+ or Clang 17+). No older-standard fallback is used.") +endif() + include(CheckSymbolExists) include(CheckLibraryExists) include(CMakeDependentOption) @@ -32,7 +36,7 @@ include(Sanitizers) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/docs/Cpp26.md b/docs/Cpp26.md new file mode 100644 index 0000000000..e033dd4ffb --- /dev/null +++ b/docs/Cpp26.md @@ -0,0 +1,117 @@ +# C++26 migration and accuracy-preserving optimization + +## Scope + +Project-owned C++ targets now require **C++26 mode** and **CMake 3.30+**. +The core exports `cxx_std_26` to consumers; the former `src/CMakeLists.txt` +C++17 override is removed. The Qt frontend, network utilities and regression +suite inherit the new mode. Vendored projects retain their own requirements +(for example Teakra still selects C++17); C sources remain C11. + +C++26 mode does not mean every C++26 language/library feature is implemented. +GCC 14+ and Clang 17+ are candidate toolchains for this mode, not a claim that +every platform combination was tested. The Linux CI explicitly uses GCC 14 +and Clang 18, and CMake 3.31.6. The application workflows remain responsible for +platform build coverage. Unsupported toolchains fail rather than falling back. + +The first library modernization uses the established `` operations +`std::countr_zero` / `std::countl_zero` (introduced in C++20), not experimental +reflection, contracts or a new SIMD API. Merely selecting C++26 is not claimed +to improve performance. + +## Changes to invalidation bitfields + +`NonStupidBitfield.h` iterates memory-invalidation bits used by GPU VRAM +tracking. Clear the lowest set bit with `x &= x - 1`, removing the dependency +on constructing a shifted mask from the counted bit index. Use standard bit +operations instead of compiler-specific builtins. + +Two independently reproduced edge cases are fixed: + +- Empty `SetRange` / `CheckRange` operations return before a shift by 64 or a + possible access past the final word (an empty range may start at `Size`). +- Iterator termination checks the absolute bit index, not just the bit index + within a 64-bit word. Padding bits in a partial final word are not returned. + +Storage layout, guest timing, CPU instructions, DMA behavior, JIT emitters and +savestate format are unchanged. No game-compatibility improvement is claimed +from these edge cases without an observed game reproduction. + +## Verification + +The baseline header `d471f0afdd31c495c1d154a6b2acc0e8e14a5e45` was retrieved +from GitHub and its Git blob hash was checked before local testing. +The baseline passed valid nonempty-range tests, failed the padding test, and +UBSan rejected a 64-bit shift in the empty-range test. The modified header +passed the tests with GCC 14.2 and Clang 17, including ASan/UBSan. + +The committed suite adds a C++26-mode compile assertion and the bitfield tests +to the seven existing CTest entries. It also compiles the complete core with +JIT enabled and disabled in CI. A compiled core is not an executed JIT test; +record per-job CI results separately. Local testing used a source fixture, +not a full application checkout. Real GPU/game/hardware tests were not run. + +## Measured microbenchmark + +Same harness and **same C++26 flags for both implementations**: +`-std=c++2c -O3 -fwrapv`, no `-march=native`, no added AVX requirement, no LTO. +AMD EPYC 9V74 virtual host, one allowed logical CPU, deterministic inputs, +one warmup per variant, nine randomized-order paired samples. Report medians; +checksums matched for every pair. Each sample scans 128 1024-bit fields for +1024 iterations. Inputs are mutated each iteration to prevent loop hoisting; +therefore the case named `empty` starts empty but is not permanently empty. + +| Compiler | Initial pattern | Before (ms) | After (ms) | Time reduction | +| --- | --- | ---: | ---: | ---: | +| GCC 14.2 | empty | 0.979 | 0.836 | 14.65% | +| GCC 14.2 | sparse | 2.965 | 2.192 | 26.06% | +| GCC 14.2 | mixed | 71.412 | 43.396 | 39.23% | +| GCC 14.2 | dense | 141.530 | 82.109 | 41.98% | +| Clang 17 | empty | 0.863 | 0.873 | -1.17% | +| Clang 17 | sparse | 2.148 | 1.843 | 14.19% | +| Clang 17 | mixed | 52.214 | 43.839 | 16.04% | +| Clang 17 | dense | 110.353 | 91.910 | 16.71% | + +These are **routine times, not emulator FPS**. Results are environment- and +compiler-dependent, and a shared VM is not a dedicated performance lab. +Do not turn these numbers into fixed CI timing thresholds. + +To compare on another machine, build `tests/BitfieldBenchmark.cpp` against +before/after source directories with the same compiler/flags, alternate runs +for `empty`, `sparse`, `mixed`, `dense`, and verify equal checksums. The current +version can also be built with: + +```sh +cmake -S tests -B build-bench -DCMAKE_BUILD_TYPE=Release -DMELONDS_BUILD_MICROBENCHMARKS=ON +cmake --build build-bench --target BitfieldBenchmark +./build-bench/BitfieldBenchmark mixed +``` + +## Acceptance policy for subsequent emulation work + +A hardware-accuracy change needs a reproducible test ROM/input and an observed +DS/DSi result (record console revision, firmware/boot mode and test hash). +Agreement with another emulator is supporting evidence, not the hardware oracle. +A test passing on the current interpreter alone does not prove accurate timing. + +An optimization must preserve guest-visible results: ordering of events, +integer rounding, memory aliases, IO side effects, DMA/IRQ behavior, and state +restoration. Compare interpreter/JIT and relevant renderers, with exact +frame/audio/state comparisons where meaningful; do not blanket-hash structures +containing pointers, padding or wall-clock-dependent data. + +Measure matched workloads with median and tail frame time. Keep host throughput +separate from the speed of the emulated hardware. Do not skip guest work, +change timing constants, enable fast-math, or add unchecked assumptions solely +to improve a benchmark. SIMD/assembly stays optional and evidence-driven. + +The next hardware-oriented candidates remain FreeBIOS unsigned square root +(including generated BIOS data), DSi direct-boot state, and rendering/capture +regressions. Those are not silently included in this migration. + +## References + +- https://gcc.gnu.org/projects/cxx-status.html +- https://clang.llvm.org/cxx_status.html +- https://cmake.org/cmake/help/latest/release/3.30.html +- https://libcxx.llvm.org/Status/Cxx26.html diff --git a/docs/Performance.md b/docs/Performance.md new file mode 100644 index 0000000000..a384e87af4 --- /dev/null +++ b/docs/Performance.md @@ -0,0 +1,57 @@ +# Performance direction for this fork + +## Decision + +Keep the existing C++ core, Qt frontend, and native JIT backends. No Rust/Zig +rewrite, handwritten assembly, blanket AVX-512 requirement, or speedup claim is +part of the initial regression fixes. There is no melonDS A/B measurement here +that establishes a benefit from changing the implementation language. + +A future isolated Rust or Zig component should justify its boundary through +measurable performance or maintainability/safety benefits. Do not introduce +per-instruction or per-pixel language crossings just to mix languages. Preserve +guest-visible timing, integer behavior, memory mapping, and save compatibility. + +## Measurement before specialization + +Compare identical scenes and inputs, recording the commit, build flags, CPU, +GPU/driver, renderer, JIT settings, resolution, and speed-limiter state. Measure +both median and tail frame time; separate emulation CPU time, rendering, +readbacks/uploads, audio, and synchronization. Distinguish host throughput from +guest timing accuracy. Use frame/audio/state comparisons appropriate to the +change instead of treating a successful build as a compatibility test. + +Investigate common 2D/capture/readback paths before assuming that a 3D backend +or CPU instruction set is the bottleneck. Compare software, classic OpenGL, +and compute modes on the same affected scene. Do not defer GPU readback past a +guest CPU/DMA read that requires the result. + +For software rasterization, compare any depth-test dispatch change in isolation; +do not attribute a combined depth-test and alpha-blend benchmark to one patch. +Profile-guided optimization should use representative workloads, with separate +build directories and an unchanged compiler/configuration between collection +and use. Existing release LTO is not a newly introduced optimization. + +## SIMD and assembly + +Prefer a clear scalar implementation and compiler-generated code first, then +intrinsics for a measured batch-processing bottleneck. Compare scalar/SSE, +AVX2, and relevant AVX-512 variants where available. Verify exact integer results, +unaligned inputs, short spans, and tails. Dispatch outside the pixel loop. + +AVX-512 is not one feature bit: check the subsets used by a kernel and OS state +support. Keep a portable fallback; do not compile distributable binaries with +blanket `-march=native` or AVX-512 assumptions. Wider instructions are not proof of +lower total frame time. CPU model and workload matter, including frequency and +memory-bandwidth effects. Handwritten assembly needs an advantage over compiler +output large enough to justify ABI and cross-platform maintenance. + +Potential candidates, only after profiling: texture/color conversion, bulk +blending, and framebuffer operations. A serial chain of emulated ARM operations +or a blocking GPU readback cannot be fixed merely by selecting wider vectors. + +## References + +- Clang optimization and PGO: https://clang.llvm.org/docs/UsersManual.html +- Rust architecture intrinsics and runtime dispatch: https://doc.rust-lang.org/stable/core/arch/ +- Zig optimization-mode safety semantics: https://ziglang.org/documentation/0.15.2/ diff --git a/src/ARMJIT.cpp b/src/ARMJIT.cpp index 4c46eea4d6..082684f77d 100644 --- a/src/ARMJIT.cpp +++ b/src/ARMJIT.cpp @@ -511,7 +511,7 @@ void ARMJIT::SetJITArgs(JITArgs args) noexcept void ARMJIT::SetMaxBlockSize(int size) noexcept { - SetJITArgs(JITArgs{static_cast(size), LiteralOptimizations, LiteralOptimizations, FastMemory}); + SetJITArgs(JITArgs{static_cast(size), LiteralOptimizations, BranchOptimizations, FastMemory}); } void ARMJIT::SetLiteralOptimizations(bool enabled) noexcept diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d322e2b148..8b2f0faebb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,3 @@ -set (CMAKE_CXX_STANDARD 17) - include(FixInterfaceIncludes) add_library(core STATIC @@ -85,6 +83,9 @@ add_library(core STATIC blip-buf/blip_buf.c) +# Public headers use modern standard library facilities; consumers need this mode too. +target_compile_features(core PUBLIC cxx_std_26) + if (ENABLE_GDBSTUB) message(NOTICE "Enabling GDB stub") target_sources(core PRIVATE diff --git a/src/FATStorage.cpp b/src/FATStorage.cpp index c9dbe7dbd5..199ad05fbe 100644 --- a/src/FATStorage.cpp +++ b/src/FATStorage.cpp @@ -24,6 +24,7 @@ #include "FATIO.h" #include "FATStorage.h" #include "Platform.h" +#include "UTF8.h" namespace melonDS { @@ -433,7 +434,7 @@ bool FATStorage::ExportFile(const std::string& path, fs::path out) err); } - fout = OpenFile(out.u8string(), FileMode::Write); + fout = OpenFile(UTF8ToString(out.u8string()), FileMode::Write); if (!fout) { f_close(&file); @@ -851,7 +852,7 @@ bool FATStorage::ImportFile(const std::string& path, fs::path in) FileHandle* fin; FRESULT res; - fin = Platform::OpenFile(in.u8string(), FileMode::Read); + fin = Platform::OpenFile(UTF8ToString(in.u8string()), FileMode::Read); if (!fin) return false; @@ -902,7 +903,7 @@ bool FATStorage::ImportDirectory(const std::string& sourcedir) // * files will be added if they aren't in the index, or if the size or last-modified-date don't match for (auto& entry : fs::recursive_directory_iterator(fs::u8path(sourcedir))) { - std::string fullpath = entry.path().u8string(); + std::string fullpath = UTF8ToString(entry.path().u8string()); std::string innerpath = fullpath.substr(srclen); if (innerpath[0] == '/' || innerpath[0] == '\\') innerpath = innerpath.substr(1); @@ -1018,7 +1019,7 @@ bool FATStorage::Load(const std::string& filename, u64 size, const std::optional // 'auto' size management: (size=0) // * if an index exists: the size from the index is used // * if no index, and an image file exists: the file size is used - // * if no image: if sourcing from a directory, size is calculated from that + // * if sourcing from a directory, size is calculated from that // with a minimum 128MB extra, otherwise size is defaulted to 512MB bool isnew = !Platform::LocalFileExists(filename); @@ -1162,4 +1163,4 @@ bool FATStorage::Save() return true; } -} \ No newline at end of file +} diff --git a/src/GPU_OpenGL.cpp b/src/GPU_OpenGL.cpp index 48533694e9..49a850b831 100644 --- a/src/GPU_OpenGL.cpp +++ b/src/GPU_OpenGL.cpp @@ -894,7 +894,7 @@ void GLRenderer::SyncVRAMCapture(u32 bank, u32 start, u32 len, bool complete) u32 pos = start; for (u32 i = 0; i < len;) { - u32 end = pos + len; + u32 end = pos + (len - i); if (end > 4) end = 4; diff --git a/src/NonStupidBitfield.h b/src/NonStupidBitfield.h index d471f0afdd..308972a546 100644 --- a/src/NonStupidBitfield.h +++ b/src/NonStupidBitfield.h @@ -25,6 +25,7 @@ #include #include +#include namespace melonDS { @@ -118,10 +119,10 @@ struct NonStupidBitField done:; } - BitIdx = __builtin_ctzll(RemainingBits); - RemainingBits &= ~(1ULL << BitIdx); + BitIdx = std::countr_zero(RemainingBits); + RemainingBits &= RemainingBits - 1; - if ((Size & 0x3F) && BitIdx >= Size) + if ((Size & 0x3F) && DataIdx * 64 + BitIdx >= Size) DataIdx = DataLength; } @@ -175,9 +176,9 @@ struct NonStupidBitField { if (Data[i]) { - u32 idx = __builtin_ctzll(Data[i]); + u32 idx = std::countr_zero(Data[i]); if (idx + i * 64 < Size) - return {*this, i, idx, Data[i] & ~(1ULL << idx)}; + return {*this, i, idx, Data[i] & (Data[i] - 1)}; } } return End(); @@ -200,6 +201,9 @@ struct NonStupidBitField void SetRange(u32 startBit, u32 bitsCount) { + if (bitsCount == 0) + return; + u32 startEntry = startBit >> 6; u64 entriesCount = (((startBit + bitsCount + 0x3F) & ~0x3F) >> 6) - startEntry; @@ -221,6 +225,9 @@ struct NonStupidBitField bool CheckRange(u32 startBit, u32 bitsCount) { + if (bitsCount == 0) + return false; + u32 startEntry = startBit >> 6; u64 entriesCount = (((startBit + bitsCount + 0x3F) & ~0x3F) >> 6) - startEntry; u64 res = 0; @@ -248,7 +255,7 @@ struct NonStupidBitField for (int i = 0; i < DataLength; i++) { if (Data[i]) - return i * 64 + __builtin_ctzll(Data[i]); + return i * 64 + std::countr_zero(Data[i]); } return -1; } @@ -258,7 +265,7 @@ struct NonStupidBitField for (int i = DataLength - 1; i >= 0; i--) { if (Data[i]) - return i * 64 + (63 - __builtin_clzll(Data[i])); + return i * 64 + (63 - std::countl_zero(Data[i])); } return -1; } diff --git a/src/Savestate.cpp b/src/Savestate.cpp index c2f01f7c68..fd33df8781 100644 --- a/src/Savestate.cpp +++ b/src/Savestate.cpp @@ -367,6 +367,21 @@ u32 Savestate::FindSection(const char* magic) const for (u32 offset = 0x10; offset < buffer_length;) { // Until we've found the desired section... + if (buffer_length - offset < 16) + { + Log(LogLevel::Error, "savestate: truncated section header\n"); + return NO_SECTION; + } + + // Validate every section, including the one we are looking for. + u32 section_length = 0; + memcpy(§ion_length, buffer + offset + 4, sizeof(section_length)); + if (section_length < 16 || section_length > buffer_length - offset) + { + Log(LogLevel::Error, "savestate: invalid section length %u\n", section_length); + return NO_SECTION; + } + // Get this section's magic number char read_magic[4] = {0}; memcpy(read_magic, buffer + offset, sizeof(read_magic)); @@ -376,18 +391,6 @@ u32 Savestate::FindSection(const char* magic) const return offset + 16; // ...return the offset of the first byte of the section after the header } - // Haven't found our section yet. Let's move on to the next one. - - u32 section_length_offset = offset + sizeof(read_magic); - if (section_length_offset >= buffer_length) - { // If trying to read the section length would take us past the file's end... - break; - } - - // First we need to find out how big this section is... - u32 section_length = 0; - memcpy(§ion_length, buffer + section_length_offset, sizeof(section_length)); - // ...then skip it. (The section length includes the 16-byte header.) offset += section_length; } diff --git a/src/UTF8.h b/src/UTF8.h new file mode 100644 index 0000000000..b0ec160c65 --- /dev/null +++ b/src/UTF8.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef MELONDS_UTF8_H +#define MELONDS_UTF8_H + +#include +#include + +namespace melonDS +{ +// Platform file APIs use UTF-8 bytes in std::string, not the native code page. +inline std::string UTF8ToString(std::u8string_view text) +{ + if (text.empty()) + return {}; + return {reinterpret_cast(text.data()), text.size()}; +} +} + +#endif // MELONDS_UTF8_H diff --git a/tests/Bitfield.cpp b/tests/Bitfield.cpp new file mode 100644 index 0000000000..16f43a6280 --- /dev/null +++ b/tests/Bitfield.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include "NonStupidBitfield.h" + +using namespace melonDS; + +static bool EmptyRanges() +{ + NonStupidBitField<128> bits; + for (u32 start : {0u, 1u, 63u, 64u, 127u, 128u}) + { + bits.SetRange(start, 0); + if (bits || bits.CheckRange(start, 0)) + return false; + } + bits.SetRange(0, 128); + for (u32 start : {0u, 1u, 63u, 64u, 127u, 128u}) + { + bits.SetRange(start, 0); + if (bits.CheckRange(start, 0) || bits.Min() != 0 || bits.Max() != 127) + return false; + } + return true; +} + +static bool Padding() +{ + NonStupidBitField<65> bits; + bits.Data[1] = ~u64{0}; + auto it = bits.Begin(); + if (it == bits.End() || *it != 64) + return false; + ++it; + if (it != bits.End()) + return false; + bits.Data[0] = 1; + bits.Data[1] = ~u64{1}; // Padding only in the final word. + auto second = bits.Begin(); + if (*second != 0) + return false; + ++second; + return second == bits.End(); +} + +template +static bool Ranges() +{ + for (u32 start = 0; start < Size; ++start) + { + for (u32 count = 1; count <= Size - start; ++count) + { + NonStupidBitField bits(start, count); + if (bits.Min() != static_cast(start) || + bits.Max() != static_cast(start + count - 1)) + return false; + u32 expected = start; + for (auto it = bits.Begin(); it != bits.End(); ++it) + { + if (*it != expected++ || expected > start + count) + return false; + } + if (expected != start + count || !bits.CheckRange(start, count)) + return false; + if (start && bits.CheckRange(0, start)) + return false; + if (start + count < Size && bits.CheckRange(start + count, Size - start - count)) + return false; + } + } + return true; +} + +static bool SparseAndDense() +{ + u64 random = 0x243f6a8885a308d3ULL; + for (u32 sample = 0; sample < 128; ++sample) + { + NonStupidBitField<1024> bits; + for (u64& word : bits.Data) + { + random ^= random << 13; + random ^= random >> 7; + random ^= random << 17; + word = sample == 0 ? 0 : sample == 1 ? ~u64{0} : random; + } + u32 next = 0; + for (auto it = bits.Begin(); it != bits.End(); ++it) + { + while (next < 1024 && !bits[next]) + ++next; + if (next == 1024 || *it != next++) + return false; + } + while (next < 1024 && !bits[next]) + ++next; + if (next != 1024) + return false; + } + return true; +} + +int main(int argc, char** argv) +{ + const char* mode = argc > 1 ? argv[1] : "all"; + bool passed = true; + if (!strcmp(mode, "empty") || !strcmp(mode, "all")) + passed = EmptyRanges() && passed; + if (!strcmp(mode, "padding") || !strcmp(mode, "all")) + passed = Padding() && passed; + if (!strcmp(mode, "ranges") || !strcmp(mode, "all")) + passed = Ranges<1>() && Ranges<65>() && Ranges<128>() && SparseAndDense() && passed; + if (strcmp(mode, "all") && strcmp(mode, "empty") && strcmp(mode, "padding") && strcmp(mode, "ranges")) + return 2; + printf("bitfield %s: %s\n", mode, passed ? "PASS" : "FAIL"); + return passed ? 0 : 1; +} diff --git a/tests/BitfieldBenchmark.cpp b/tests/BitfieldBenchmark.cpp new file mode 100644 index 0000000000..2f4f3abf50 --- /dev/null +++ b/tests/BitfieldBenchmark.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Build the same file against the before/after headers; compare equal checksums. +#include +#include +#include +#include +#include "NonStupidBitfield.h" + +using namespace melonDS; +using Field = NonStupidBitField<1024>; + +int main(int argc, char** argv) +{ + if (argc != 2) + return 2; + Field fields[128]; + u64 random = 0x243f6a8885a308d3ULL; + for (Field& field : fields) + { + for (u64& word : field.Data) + { + random ^= random << 13; + random ^= random >> 7; + random ^= random << 17; + if (!strcmp(argv[1], "empty")) word = 0; + else if (!strcmp(argv[1], "sparse")) word = 1ULL << (random & 63); + else if (!strcmp(argv[1], "mixed")) word = random; + else if (!strcmp(argv[1], "dense")) word = ~u64{0}; + else return 2; + } + } + u64 checksum = 0; + constexpr unsigned iterations = 1024; + auto start = std::chrono::steady_clock::now(); + for (unsigned repeat = 0; repeat < iterations; ++repeat) + { + // Vary input each pass so the scan cannot be hoisted out of this loop. + fields[repeat % 128].Data[(repeat / 128) % 16] ^= 1ULL << (repeat & 63); + for (Field& field : fields) + for (auto it = field.Begin(); it != field.End(); ++it) + checksum += *it; + } + auto elapsed = std::chrono::steady_clock::now() - start; + double ns = std::chrono::duration(elapsed).count(); + printf("%s,%.0f,%llu\n", argv[1], ns, static_cast(checksum)); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..7512db13ef --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.30) +project(melonDSCoreRegressionTests LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 26) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +enable_testing() + +# Standalone: no Qt, SDL, BIOS images, ROMs, or full core build required. +add_executable(SavestateSections SavestateSections.cpp ../src/Savestate.cpp) +target_include_directories(SavestateSections PRIVATE ../src) +target_compile_features(SavestateSections PRIVATE cxx_std_26) + +foreach(case roundtrip invalid-length truncated-header zero-length-skip oversized-skip) + add_test(NAME savestate-${case} COMMAND SavestateSections ${case}) + set_tests_properties(savestate-${case} PROPERTIES TIMEOUT 5) +endforeach() + +find_package(Python3 REQUIRED COMPONENTS Interpreter) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/CaptureMethod.inc" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/ExtractFunction.py" + "${CMAKE_CURRENT_SOURCE_DIR}/../src/GPU_OpenGL.cpp" + "void GLRenderer::SyncVRAMCapture(u32 bank, u32 start, u32 len, bool complete)" + "${CMAKE_CURRENT_BINARY_DIR}/CaptureMethod.inc" + DEPENDS ExtractFunction.py ../src/GPU_OpenGL.cpp + VERBATIM) +add_executable(CaptureReadback CaptureReadback.cpp "${CMAKE_CURRENT_BINARY_DIR}/CaptureMethod.inc") +target_include_directories(CaptureReadback PRIVATE ../src "${CMAKE_CURRENT_BINARY_DIR}") +target_compile_features(CaptureReadback PRIVATE cxx_std_26) +add_test(NAME capture-readback-ranges COMMAND CaptureReadback) +set_tests_properties(capture-readback-ranges PROPERTIES TIMEOUT 10) + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/JitMethod.inc" "${CMAKE_CURRENT_BINARY_DIR}/JitArgs.inc" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/ExtractFunction.py" + "${CMAKE_CURRENT_SOURCE_DIR}/../src/ARMJIT.cpp" + "void ARMJIT::SetMaxBlockSize(int size) noexcept" + "${CMAKE_CURRENT_BINARY_DIR}/JitMethod.inc" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/ExtractFunction.py" + "${CMAKE_CURRENT_SOURCE_DIR}/../src/Args.h" + "struct JITArgs" + "${CMAKE_CURRENT_BINARY_DIR}/JitArgs.inc" + DEPENDS ExtractFunction.py ../src/ARMJIT.cpp ../src/Args.h + VERBATIM) +add_executable(JitSettings JitSettings.cpp + "${CMAKE_CURRENT_BINARY_DIR}/JitMethod.inc" "${CMAKE_CURRENT_BINARY_DIR}/JitArgs.inc") +target_include_directories(JitSettings PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") +target_compile_features(JitSettings PRIVATE cxx_std_26) +add_test(NAME jit-setting-forwarding COMMAND JitSettings) +set_tests_properties(jit-setting-forwarding PROPERTIES TIMEOUT 5) + +add_executable(LanguageStandard LanguageStandard.cpp) +target_compile_features(LanguageStandard PRIVATE cxx_std_26) +add_test(NAME cxx26-language-mode COMMAND LanguageStandard) + +add_executable(Bitfield Bitfield.cpp) +target_include_directories(Bitfield PRIVATE ../src) +target_compile_features(Bitfield PRIVATE cxx_std_26) +add_test(NAME bitfield-boundaries COMMAND Bitfield all) +set_tests_properties(bitfield-boundaries PROPERTIES TIMEOUT 15) + +option(MELONDS_BUILD_MICROBENCHMARKS "Build standalone microbenchmarks (not performance gates)" OFF) +if (MELONDS_BUILD_MICROBENCHMARKS) + add_executable(BitfieldBenchmark BitfieldBenchmark.cpp) + target_include_directories(BitfieldBenchmark PRIVATE ../src) + target_compile_features(BitfieldBenchmark PRIVATE cxx_std_26) +endif() + +add_executable(UTF8Paths UTF8Paths.cpp) +target_include_directories(UTF8Paths PRIVATE ../src) +target_compile_features(UTF8Paths PRIVATE cxx_std_26) +add_test(NAME utf8-path-roundtrip COMMAND UTF8Paths) diff --git a/tests/CaptureReadback.cpp b/tests/CaptureReadback.cpp new file mode 100644 index 0000000000..171887fed5 --- /dev/null +++ b/tests/CaptureReadback.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Compile the production method with a recording GL boundary. This tests byte +// ranges and dirty flags, NOT OpenGL drivers, shader output, or game timing. +#include +#include +#include +#include "types.h" + +using namespace melonDS; + +enum LogLevel { Error }; +static void Log(LogLevel, const char*, ...) {} +enum { GL_DITHER, GL_READ_FRAMEBUFFER, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV }; +static void glDisable(int) {} +static void glBindFramebuffer(int, int) {} +static unsigned BytesWritten; +static void glReadPixels(int, int, int width, int height, int, int, void* dst) +{ + unsigned size = static_cast(width * height * 2); + memset(dst, 0xA5, size); + BytesWritten += size; +} + +struct GPUFixture +{ + u8 VRAM[4][128 * 1024] {}; + bool VRAMDirty[4][256] {}; +}; + +class GLRenderer +{ +public: + GPUFixture& GPU; + int CaptureSyncFB = 0; + explicit GLRenderer(GPUFixture& gpu) : GPU(gpu) {} + void DownscaleCapture(int, int, int) {} + void SyncVRAMCapture(u32 bank, u32 start, u32 len, bool complete); +}; + +#include "CaptureMethod.inc" + +int main() +{ + int failures = 0; + for (u32 start = 0; start < 4; start++) + { + for (u32 len = 0; len < 4; len++) + { + GPUFixture gpu; + GLRenderer renderer(gpu); + BytesWritten = 0; + renderer.SyncVRAMCapture(2, start, len, true); + unsigned blocks = len == 0 ? 1 : len; + bool okay = BytesWritten == blocks * 32768; + for (u32 bank = 0; bank < 4; bank++) + { + for (u32 offset = 0; offset < 128 * 1024; offset++) + { + bool expected = bank == 2 && + ((offset / 32768 + 4 - start) % 4) < blocks; + if (gpu.VRAM[bank][offset] != (expected ? 0xA5 : 0) || + gpu.VRAMDirty[bank][offset / 512] != expected) + okay = false; + } + } + if (!okay) + { + fprintf(stderr, "Capture start=%u len=%u wrote %u bytes, expected %u\n", + start, len, BytesWritten, blocks * 32768); + failures++; + } + } + } + printf("Capture range cases: %d/16 passed\n", 16 - failures); + return failures ? 1 : 0; +} diff --git a/tests/ExtractFunction.py b/tests/ExtractFunction.py new file mode 100644 index 0000000000..580ea71614 --- /dev/null +++ b/tests/ExtractFunction.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Extract a current source definition for dependency-isolated regression tests. + +This follows melonDS's unindented function-closing brace convention. It does not +parse general C++; changed formatting is deliberately an error, not a fallback +to a stale copy of the implementation. +""" +import pathlib +import sys + +source, signature, output = sys.argv[1:] +text = pathlib.Path(source).read_text(encoding="utf-8") +needle = signature + "\n{\n" +if text.count(needle) != 1: + raise SystemExit(f"Expected exactly one definition of {signature!r}") +start = text.index(needle) +end = text.index("\n}", start) + 2 +body = text[start:end] +if body.count("{") != body.count("}"): + raise SystemExit("Function extraction needs updating: unbalanced braces") +pathlib.Path(output).write_text(body + "\n", encoding="utf-8") diff --git a/tests/JitSettings.cpp b/tests/JitSettings.cpp new file mode 100644 index 0000000000..9478d4fff4 --- /dev/null +++ b/tests/JitSettings.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This isolates argument forwarding, not native JIT emission or execution. +#include +#include "JitArgs.inc" +; + +class ARMJIT +{ +public: + bool LiteralOptimizations; + bool BranchOptimizations; + bool FastMemory; + JITArgs Received; + void SetJITArgs(JITArgs args) noexcept { Received = args; } + void SetMaxBlockSize(int size) noexcept; +}; + +#include "JitMethod.inc" + +int main() +{ + int failures = 0; + for (unsigned flags = 0; flags < 8; flags++) + { + ARMJIT jit {}; + jit.LiteralOptimizations = (flags & 1) != 0; + jit.BranchOptimizations = (flags & 2) != 0; + jit.FastMemory = (flags & 4) != 0; + jit.SetMaxBlockSize(16); + const auto& args = jit.Received; + if (args.MaxBlockSize != 16 || + args.LiteralOptimizations != jit.LiteralOptimizations || + args.BranchOptimizations != jit.BranchOptimizations || + args.FastMemory != jit.FastMemory) + { + fprintf(stderr, "JIT setter changed unrelated flags: %u\n", flags); + failures++; + } + } + printf("JIT settings cases: %d/8 passed\n", 8 - failures); + return failures ? 1 : 0; +} diff --git a/tests/LanguageStandard.cpp b/tests/LanguageStandard.cpp new file mode 100644 index 0000000000..926851d15b --- /dev/null +++ b/tests/LanguageStandard.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include + +#ifdef _MSVC_LANG +constexpr long LanguageVersion = _MSVC_LANG; +#else +constexpr long LanguageVersion = __cplusplus; +#endif + +static_assert(LanguageVersion > 202302L, "The regression suite must compile in C++26 mode"); +static_assert(__cpp_lib_bitops >= 201907L, "Standard bit operations are required"); +static_assert(std::countr_zero(0ULL) == 64); +static_assert(std::countl_zero(0ULL) == 64); + +int main() +{ + printf("C++ language mode: %ld; standard bit operations available\n", LanguageVersion); +} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..13a8c3c861 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,58 @@ +# Focused core regression tests + +Run from a complete melonDS source checkout. These tests require a compiler supporting C++26 mode, +CMake 3.30 or newer, and Python 3; Ninja is optional. They do not require Qt, SDL, +BIOS images, ROMs, or a GPU context. + +```sh +cmake -S tests -B build-regression -DCMAKE_BUILD_TYPE=Debug +cmake --build build-regression +ctest --test-dir build-regression --output-on-failure +``` + +For a separate Clang AddressSanitizer/UndefinedBehaviorSanitizer build on Linux: + +```sh +cmake -S tests -B build-regression-sanitized \ + -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" +cmake --build build-regression-sanitized +ctest --test-dir build-regression-sanitized --output-on-failure +``` + +## Coverage and limits + +There are ten CTest entries. No real DS/DSi, ROM, GPU driver or filesystem +mutation is exercised. The optional microbenchmark measures only bitfield +iteration, not emulator FPS; see `../docs/Cpp26.md`. + +- `LanguageStandard` verifies the requested C++26 mode and standard bit operations. +- `Bitfield` checks valid ranges, zero-length boundaries, partial-word padding, + and sparse/dense iteration against a bitwise reference. +- `UTF8Paths` checks byte-preserving filesystem-path conversion for ASCII, + Korean, Japanese, non-BMP, long and empty strings, plus embedded-NUL byte + conversion. It does not open or modify files. + +- `SavestateSections` compiles the actual `src/Savestate.cpp`; only frontend + logging is replaced. Five cases cover valid/reordered/header-only sections, + invalid matching lengths, truncated headers, zero-length skips, and oversized + skips. The zero-length case has a timeout to catch non-advancing iteration. +- `CaptureReadback` compiles the current `GLRenderer::SyncVRAMCapture` definition + extracted from `src/GPU_OpenGL.cpp` with a recording OpenGL boundary and a small + VRAM fixture. All 16 start/size combinations check written bytes and dirty + flags, including the 128x128 special case. It does not test shader output, + real OpenGL drivers, frame timing, or complete GPU state. +- `JitSettings` extracts the current `ARMJIT::SetMaxBlockSize` definition and the + actual `JITArgs` layout. Eight flag combinations check argument forwarding to + a recording `SetJITArgs` boundary, not native code generation, cache resets, + or JIT execution. + +The extraction script deliberately follows the current source formatting and +fails on a missing/non-unique definition or unbalanced braces. It is not a +C++ parser and does not keep a second, potentially stale implementation. Keep +these tests narrow; use integrated core/game tests for broader changes. + +Passing this suite is not evidence of complete emulator compatibility or a +measured performance improvement. No savestate format version is changed by +these fixes. Section validation is not a complete malformed-state audit. diff --git a/tests/SavestateSections.cpp b/tests/SavestateSections.cpp new file mode 100644 index 0000000000..aef80a3e5c --- /dev/null +++ b/tests/SavestateSections.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include +#include "Savestate.h" +#include "Platform.h" + +using namespace melonDS; + +// The section parser has no frontend dependency other than logging. +void melonDS::Platform::Log(LogLevel, const char*, ...) {} + +static std::vector MakeState(u32 size) +{ + std::vector data(size, 0); + memcpy(data.data(), "MELN", 4); + u16 major = SAVESTATE_MAJOR; + u16 minor = SAVESTATE_MINOR; + memcpy(data.data() + 4, &major, sizeof(major)); + memcpy(data.data() + 6, &minor, sizeof(minor)); + memcpy(data.data() + 8, &size, sizeof(size)); + return data; +} + +static bool Rejects(std::vector& data, const char* magic) +{ + Savestate state(data.data(), static_cast(data.size()), false); + if (state.Error) return false; // The global header must be valid in these tests. + state.Section(magic); + return state.Error; +} + +int main(int argc, char** argv) +{ + if (argc != 2) return 2; + if (!strcmp(argv[1], "roundtrip")) + { + Savestate saved(128); + u32 first = 0x12345678, second = 0xABCDEF01; + saved.Section("ONE1"); + saved.Var32(&first); + saved.Section("TWO2"); + saved.Var32(&second); + saved.Section("EMPT"); + saved.Finish(); + if (saved.Error) return 1; + Savestate loaded(saved.Buffer(), saved.Length(), false); + u32 result = 0; + loaded.Section("TWO2"); + loaded.Var32(&result); + if (loaded.Error || result != second) return 1; + loaded.Section("ONE1"); + loaded.Var32(&result); + if (loaded.Error || result != first) return 1; + loaded.Section("EMPT"); + if (loaded.Error) return 1; // A header-only (16-byte) section is valid. + loaded.Section("MISS"); + return loaded.Error ? 0 : 1; + } + if (!strcmp(argv[1], "invalid-length")) + { + for (u32 size : {0u, 1u, 15u, 17u, 0xFFFFFFFFu}) + { + auto data = MakeState(32); + memcpy(data.data() + 16, "TEST", 4); + memcpy(data.data() + 20, &size, sizeof(size)); + if (!Rejects(data, "TEST")) + { + fprintf(stderr, "Accepted matching section with invalid length %u\n", size); + return 1; + } + } + return 0; + } + if (!strcmp(argv[1], "truncated-header")) + { + for (u32 tail = 1; tail < 16; tail++) + { + auto data = MakeState(16 + tail); + memcpy(data.data() + 16, "TEST", tail < 4 ? tail : 4); + if (!Rejects(data, "TEST")) + { + fprintf(stderr, "Accepted truncated section header of %u bytes\n", tail); + return 1; + } + } + return 0; + } + if (!strcmp(argv[1], "zero-length-skip")) + { + auto data = MakeState(32); + memcpy(data.data() + 16, "SKIP", 4); + return Rejects(data, "TEST") ? 0 : 1; + } + if (!strcmp(argv[1], "oversized-skip")) + { + auto data = MakeState(32); + memcpy(data.data() + 16, "SKIP", 4); + u32 size = 0xFFFFFFFF; + memcpy(data.data() + 20, &size, sizeof(size)); + return Rejects(data, "TEST") ? 0 : 1; + } + return 2; +} diff --git a/tests/UTF8Paths.cpp b/tests/UTF8Paths.cpp new file mode 100644 index 0000000000..ae92dcc1ef --- /dev/null +++ b/tests/UTF8Paths.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include +#include "UTF8.h" + +int main() +{ + const std::u8string names[] = { + u8"", u8"saves/plain.sav", u8"\uD55C\uAE00/\uC800\uC7A5.sav", + u8"\u65E5\u672C\u8A9E/\u30C6\u30B9\u30C8.nds", + u8"\U0001F3AE/caf\u00E9.sav", std::u8string(512, u8'x') + }; + for (const auto& name : names) + { + const std::filesystem::path path(name); + const std::string bytes = melonDS::UTF8ToString(path.u8string()); + if (bytes.size() != name.size() || memcmp(bytes.data(), name.data(), name.size()) != 0) + return 1; + // Construct char8_t storage rather than aliasing a char buffer as char8_t. + const std::u8string restored(bytes.begin(), bytes.end()); + if (std::filesystem::path(restored) != path) + return 1; + } + const std::u8string withZero(u8"a\0b", 3); + const std::string bytes = melonDS::UTF8ToString(withZero); + if (bytes.size() != 3 || memcmp(bytes.data(), withZero.data(), 3) != 0) + return 1; + if (!melonDS::UTF8ToString({}).empty()) + return 1; + puts("UTF-8 path bytes and round trips: PASS"); +}