diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..dfe07704
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 83f1cf84..00000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,300 +0,0 @@
-name: Build
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build-glfw-linux-x86_64:
- name: Build (GLFW/Linux x86_64)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Get commit info
- id: commit-info
- run: |
- echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT
-
- - name: Install dependencies
- run: sudo apt-get update && sudo apt-get install -y cmake libglfw3-dev libbz2-dev
-
- - name: Configure
- run: cmake -B build -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}"
-
- - name: Build
- run: cmake --build build -j$(nproc)
-
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: butterscotch-glfw-linux-x86_64
- path: build/butterscotch
-
- build-glfw-windows-x86_64:
- name: Build (GLFW/Windows x86_64)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Get commit info
- id: commit-info
- run: |
- echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT
-
- - name: Install MinGW and dependencies
- run: |
- sudo apt-get update
- sudo apt-get install -y cmake mingw-w64
-
- - name: Build GLFW for MinGW
- run: |
- git clone https://github.com/glfw/glfw.git /tmp/glfw
- git -C /tmp/glfw checkout fdd14e65b1c29e4e6df875fb5669ec00d6793531
- cmake -B /tmp/glfw/build -S /tmp/glfw \
- -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/cmake/mingw-w64.cmake \
- -DCMAKE_INSTALL_PREFIX=/usr/x86_64-w64-mingw32 \
- -DGLFW_BUILD_EXAMPLES=OFF -DGLFW_BUILD_TESTS=OFF -DGLFW_BUILD_DOCS=OFF
- make -C /tmp/glfw/build -j$(nproc)
- sudo make -C /tmp/glfw/build install
-
- - name: Build bzip2 for MinGW
- run: |
- curl -L https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz -o /tmp/bzip2.tar.gz
- tar -xf /tmp/bzip2.tar.gz -C /tmp
- make -C /tmp/bzip2-1.0.8 libbz2.a CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-ar RANLIB=x86_64-w64-mingw32-ranlib -j$(nproc)
- sudo cp /tmp/bzip2-1.0.8/libbz2.a /usr/x86_64-w64-mingw32/lib/
- sudo cp /tmp/bzip2-1.0.8/bzlib.h /usr/x86_64-w64-mingw32/include/
-
- - name: Configure
- run: cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64.cmake -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}"
-
- - name: Build
- run: cmake --build build -j$(nproc)
-
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: butterscotch-glfw-windows-x86_64
- path: build/butterscotch.exe
-
- build-glfw-linux-x86:
- name: Build (GLFW/Linux x86)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Get commit info
- id: commit-info
- run: |
- echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT
-
- - name: Install 32-bit toolchain and dependencies
- run: |
- sudo dpkg --add-architecture i386
- sudo apt-get update
- sudo apt-get install -y cmake gcc-multilib g++-multilib pkg-config libbz2-dev:i386 libx11-dev:i386 libxrandr-dev:i386 libxinerama-dev:i386 libxcursor-dev:i386 libxi-dev:i386 libgl-dev:i386
-
- - name: Build GLFW (32-bit)
- run: |
- git clone https://github.com/glfw/glfw.git /tmp/glfw
- git -C /tmp/glfw checkout fdd14e65b1c29e4e6df875fb5669ec00d6793531
- cmake -B /tmp/glfw/build -S /tmp/glfw \
- -DCMAKE_C_FLAGS=-m32 \
- -DCMAKE_INSTALL_PREFIX=/usr/local/glfw-x86 \
- -DGLFW_BUILD_EXAMPLES=OFF -DGLFW_BUILD_TESTS=OFF -DGLFW_BUILD_DOCS=OFF -DGLFW_BUILD_WAYLAND=OFF
- make -C /tmp/glfw/build -j$(nproc)
- sudo make -C /tmp/glfw/build install
-
- - name: Configure
- run: |
- export PKG_CONFIG_PATH=/usr/local/glfw-x86/lib/pkgconfig:/usr/lib/i386-linux-gnu/pkgconfig
- cmake -B build -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-m32 -DCMAKE_EXE_LINKER_FLAGS=-m32 "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}"
-
- - name: Build
- run: cmake --build build -j$(nproc)
-
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: butterscotch-glfw-linux-x86
- path: build/butterscotch
-
- build-glfw-windows-x86:
- name: Build (GLFW/Windows x86)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Get commit info
- id: commit-info
- run: |
- echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT
-
- - name: Install MinGW and dependencies
- run: |
- sudo apt-get update
- sudo apt-get install -y cmake mingw-w64
-
- - name: Build GLFW for MinGW (32-bit)
- run: |
- git clone https://github.com/glfw/glfw.git /tmp/glfw
- git -C /tmp/glfw checkout fdd14e65b1c29e4e6df875fb5669ec00d6793531
- cmake -B /tmp/glfw/build -S /tmp/glfw \
- -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/cmake/mingw-w64-i686.cmake \
- -DCMAKE_INSTALL_PREFIX=/usr/i686-w64-mingw32 \
- -DGLFW_BUILD_EXAMPLES=OFF -DGLFW_BUILD_TESTS=OFF -DGLFW_BUILD_DOCS=OFF
- make -C /tmp/glfw/build -j$(nproc)
- sudo make -C /tmp/glfw/build install
-
- - name: Build bzip2 for MinGW (32-bit)
- run: |
- curl -L https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz -o /tmp/bzip2.tar.gz
- tar -xf /tmp/bzip2.tar.gz -C /tmp
- make -C /tmp/bzip2-1.0.8 libbz2.a CC=i686-w64-mingw32-gcc AR=i686-w64-mingw32-ar RANLIB=i686-w64-mingw32-ranlib -j$(nproc)
- sudo cp /tmp/bzip2-1.0.8/libbz2.a /usr/i686-w64-mingw32/lib/
- sudo cp /tmp/bzip2-1.0.8/bzlib.h /usr/i686-w64-mingw32/include/
-
- - name: Configure
- run: cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-i686.cmake -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}"
-
- - name: Build
- run: cmake --build build -j$(nproc)
-
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: butterscotch-glfw-windows-x86
- path: build/butterscotch.exe
-
- build-ps2:
- name: Build (PS2)${{ matrix.label }}
- runs-on: ubuntu-latest
- container:
- image: ps2dev/ps2dev:latest
- strategy:
- fail-fast: false
- matrix:
- include:
- - label: ""
- artifact: butterscotch-ps2
- extra_flags: ""
- - label: " [Bytecode Version 16]"
- artifact: butterscotch-ps2-bc16
- extra_flags: "-DENABLE_BC16=ON -DENABLE_BC17=OFF"
- - label: " [Bytecode Version 17]"
- artifact: butterscotch-ps2-bc17
- extra_flags: "-DENABLE_BC16=OFF -DENABLE_BC17=ON"
- steps:
- - name: Install build tools
- run: apk add --no-cache cmake make gmp-dev mpfr-dev mpc1-dev git
-
- - uses: actions/checkout@v4
-
- # Yeah, the safe.directory is needed because "fatal: detected dubious ownership in repository at '/__w/Butterscotch/Butterscotch'"
- - name: Get commit info
- id: commit-info
- run: |
- git config --global --add safe.directory "$GITHUB_WORKSPACE"
- echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- echo "date=$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
-
- - name: Configure
- run: |
- mkdir -p build
- export CFLAGS="-fopt-info-inline-optimized=$PWD/build/inline_report.txt"
- cmake -B build -DCMAKE_TOOLCHAIN_FILE=$PS2SDK/ps2dev.cmake -DPLATFORM=ps2 ${{ matrix.extra_flags }} -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}"
-
- - name: Build
- run: |
- cmake --build build -j$(nproc)
- cp build/butterscotch build/butterscotch.elf
-
- - name: Check executeLoop I-Cache fit
- run: |
- # Extract executeLoop function size(s) from the ELF symbol table.
- # GCC IPA passes (constprop/isra/part) may clone a static function and
- # suffix the clones (e.g. "executeLoop.constprop.0"). Sum all variants
- # since they all share the 16 KB I-Cache budget.
- # nm -S prints: address size type name
- echo "Getting executeLoop size from ELF symbol table..."
- SYMBOL_LINES=$(mips64r5900el-ps2-elf-nm -S build/butterscotch.elf | grep -E ' executeLoop(\..*)?$' || true)
- if [ -z "$SYMBOL_LINES" ]; then
- echo "::error::Could not find executeLoop symbol in ELF (is it stripped?)"
- exit 1
- fi
- echo "$SYMBOL_LINES"
- SIZE_DEC=0
- VARIANT_COUNT=0
- for SIZE_HEX in $(echo "$SYMBOL_LINES" | awk '{print $2}'); do
- SIZE_DEC=$((SIZE_DEC + 0x$SIZE_HEX))
- VARIANT_COUNT=$((VARIANT_COUNT + 1))
- done
- LIMIT=16384
- if [ "$SIZE_DEC" -gt "$LIMIT" ]; then
- echo "::error::executeLoop is $SIZE_DEC bytes across $VARIANT_COUNT variant(s), exceeding the PS2 16 KB I-Cache ($LIMIT bytes) by $((SIZE_DEC - LIMIT)) bytes"
- echo "| Function | Size | Limit | Status |" >> $GITHUB_STEP_SUMMARY
- echo "|----------|------|-------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| \`executeLoop\` ($VARIANT_COUNT variant(s)) | $SIZE_DEC bytes | $LIMIT bytes (16 KB) | **EXCEEDED** by $((SIZE_DEC - LIMIT)) bytes |" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- echo "| Function | Size | Limit | Status |" >> $GITHUB_STEP_SUMMARY
- echo "|----------|------|-------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| \`executeLoop\` ($VARIANT_COUNT variant(s)) | $SIZE_DEC bytes | $LIMIT bytes (16 KB) | $((LIMIT - SIZE_DEC)) bytes remaining |" >> $GITHUB_STEP_SUMMARY
-
- - name: Report executeLoop inlining
- run: |
- REPORT=build/inline_report.txt
- if [ ! -s "$REPORT" ]; then
- echo "::warning::No inline report produced at $REPORT"
- exit 0
- fi
- # Extract callees that GCC reported as inlined into executeLoop
- INLINED=$(grep -E "Inlin(ed|ing) .* into executeLoop" "$REPORT" \
- | awk '{for(i=1;i<=NF;i++) if($i=="into" && $(i+1) ~ /^executeLoop/) {name=$(i-1); sub(/\/[0-9]+$/,"",name); print name}}' \
- | sort -u)
- # Cross-check: jal targets still present in the compiled executeLoop
- # (including GCC-cloned variants like executeLoop.constprop.0).
- VARIANT_NAMES=$(mips64r5900el-ps2-elf-nm build/butterscotch.elf | awk '/ executeLoop(\..*)?$/ {print $3}')
- DISASM_ARGS=""
- for NAME in $VARIANT_NAMES; do
- DISASM_ARGS="$DISASM_ARGS --disassemble=$NAME"
- done
- NOT_INLINED=$(mips64r5900el-ps2-elf-objdump -d $DISASM_ARGS build/butterscotch.elf 2>/dev/null \
- | grep -oE "jal[[:space:]]+[0-9a-f]+ <[^>]+>" \
- | sed -E 's/.*<([^>]+)>/\1/' \
- | sort -u)
- INLINED_COUNT=$(printf "%s\n" "$INLINED" | grep -c . || true)
- NOT_INLINED_COUNT=$(printf "%s\n" "$NOT_INLINED" | grep -c . || true)
- {
- echo ""
- echo "### \`executeLoop\` inlining report"
- echo ""
- echo "Inlined into executeLoop ($INLINED_COUNT callees, per GCC -fopt-info-inline-optimized)
"
- echo ""
- echo '```'
- printf "%s\n" "$INLINED"
- echo '```'
- echo ""
- echo " "
- echo ""
- echo "Still called via jal from executeLoop ($NOT_INLINED_COUNT symbols, per objdump)
"
- echo ""
- echo '```'
- printf "%s\n" "$NOT_INLINED"
- echo '```'
- echo ""
- echo " "
- } >> $GITHUB_STEP_SUMMARY
-
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: ${{ matrix.artifact }}
- path: build/butterscotch.elf
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 180e3a3f..4ea31322 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,8 @@ build_ps2/
.gitignore
.cache
+.vscode/tasks.json
+resources/3ds/romfs/audio/
+resources/3ds/romfs/borders/
+resources/3ds/romfs/gfx/
+resources/3ds/romfs/*.bcwav
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0dfab0cb..bd7e4813 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -25,6 +25,10 @@ endif()
# performance on low end hardware!
option(ENABLE_BC16 "Enable support for Bytecode Version 16" ON)
option(ENABLE_BC17 "Enable support for Bytecode Version 17" ON)
+if(PLATFORM STREQUAL "n3ds")
+ set(ENABLE_BC16 ON CACHE BOOL "Enable support for Bytecode Version 16" FORCE)
+ set(ENABLE_BC17 OFF CACHE BOOL "Enable support for Bytecode Version 17" FORCE)
+endif()
if(NOT ENABLE_BC16 AND NOT ENABLE_BC17)
message(FATAL_ERROR "You need to build Butterscotch with at least one bytecode version enabled!")
endif()
@@ -35,6 +39,27 @@ if(ENABLE_BC17)
add_compile_definitions(ENABLE_BC17)
endif()
+set(BUTTERSCOTCH_WIIU_ICON "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/icon.png")
+set(BUTTERSCOTCH_WIIU_DRC_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/drc.png")
+set(BUTTERSCOTCH_WIIU_TV_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/tv.png")
+set(BUTTERSCOTCH_WIIU_NAME "UNDERTALE")
+set(BUTTERSCOTCH_WIIU_SHORTNAME "UNDERTALE")
+set(BUTTERSCOTCH_WIIU_APP_FOLDER "cinnamon")
+set(BUTTERSCOTCH_3DS_NAME "cinnamon")
+set(BUTTERSCOTCH_3DS_DESCRIPTION "cinnamon")
+set(BUTTERSCOTCH_3DS_AUTHOR "Project Sunshine")
+if(ENABLE_BC17 AND NOT ENABLE_BC16)
+ set(BUTTERSCOTCH_WIIU_ICON "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/icon-deltarune.png")
+ set(BUTTERSCOTCH_WIIU_DRC_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/drc-deltarune.png")
+ set(BUTTERSCOTCH_WIIU_TV_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/tv-deltarune.png")
+ set(BUTTERSCOTCH_WIIU_NAME "DELTARUNE")
+ set(BUTTERSCOTCH_WIIU_SHORTNAME "DELTARUNE")
+endif()
+option(WIIU_USE_DELTARUNE_SD_PATH "Use /wiiu/apps/deltarune instead of /wiiu/apps/cinnamon on Wii U" OFF)
+if(WIIU_USE_DELTARUNE_SD_PATH)
+ set(BUTTERSCOTCH_WIIU_APP_FOLDER "deltarune")
+endif()
+
file(GLOB SOURCES src/*.c)
# Platform specific files builds
file(GLOB PLATFORM_SOURCES src/${PLATFORM}/*.c)
@@ -53,8 +78,10 @@ target_include_directories(butterscotch PUBLIC vendor/stb/ds)
if(PLATFORM STREQUAL "glfw")
file(GLOB GL_SOURCES src/gl/*.c)
+ list(REMOVE_ITEM GL_SOURCES ${CMAKE_SOURCE_DIR}/src/gl/gl_renderer.c)
target_sources(butterscotch PRIVATE ${GL_SOURCES})
target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl)
+ target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/vendor)
# Butterscotch VM/interpreter profiler
option(ENABLE_VM_GML_PROFILER "Enable Butterscotch VM/interpreter profiler" ON)
@@ -146,6 +173,66 @@ if(PLATFORM STREQUAL "glfw")
${CMAKE_SOURCE_DIR}/vendor/gamecontrollerdb.txt
$/gamecontrollerdb.txt
)
+elseif(PLATFORM STREQUAL "wiiu")
+ add_compile_definitions(USE_FLOAT_REALS)
+ add_compile_definitions(NO_RVALUE_INT64)
+ # find_package(SDL2 CONFIG REQUIRED)
+ target_sources(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl/image_decoder.c)
+ target_compile_definitions(butterscotch PRIVATE BUTTERSCOTCH_WIIU_APP_FOLDER="${BUTTERSCOTCH_WIIU_APP_FOLDER}")
+
+ if(DEFINED ENV{DEVKITPRO} AND NOT "$ENV{DEVKITPRO}" STREQUAL "")
+ file(TO_CMAKE_PATH "$ENV{DEVKITPRO}" DEVKITPRO_ROOT)
+ elseif(EXISTS "C:/devkitPro")
+ file(TO_CMAKE_PATH "C:/devkitPro" DEVKITPRO_ROOT)
+ else()
+ message(FATAL_ERROR "DEVKITPRO is not set and C:/devkitPro was not found.")
+ endif()
+
+ # data.win and bytecode blobs are serialized little-endian, but Wii U's PowerPC CPU is big-endian.
+ target_compile_definitions(butterscotch PRIVATE IS_BIG_ENDIAN)
+
+ target_compile_options(butterscotch PRIVATE
+ -O3
+ -ffunction-sections -fdata-sections
+ -fsingle-precision-constant
+ -fomit-frame-pointer
+ -ffast-math
+ )
+
+ target_include_directories(butterscotch PRIVATE
+ ${CMAKE_SOURCE_DIR}/vendor
+ ${CMAKE_SOURCE_DIR}/vendor/stb/image
+ ${CMAKE_SOURCE_DIR}/vendor/stb/vorbis
+ ${CMAKE_SOURCE_DIR}/vendor/miniaudio
+ ${DEVKITPRO_ROOT}/wut/include
+ ${DEVKITPRO_ROOT}/portlibs/wiiu/include
+ ${DEVKITPRO_ROOT}/portlibs/ppc/include
+ )
+
+ target_link_directories(butterscotch PRIVATE
+ ${DEVKITPRO_ROOT}/portlibs/wiiu/lib
+ ${DEVKITPRO_ROOT}/portlibs/ppc/lib
+ )
+ target_link_libraries(butterscotch PRIVATE bz2 SDL2)
+
+ set_target_properties(butterscotch PROPERTIES OUTPUT_NAME undertale)
+
+ if(NOT COMMAND wut_create_rpx)
+ message(FATAL_ERROR "wut_create_rpx is unavailable. Configure with the devkitPro Wii U toolchain file.")
+ endif()
+
+ wut_create_rpx(butterscotch)
+
+ wut_create_wuhb(butterscotch
+ NAME ${BUTTERSCOTCH_WIIU_NAME}
+ SHORTNAME ${BUTTERSCOTCH_WIIU_SHORTNAME}
+ AUTHOR "Toby Fox, Project Sunshine"
+ DRCSPLASH ${BUTTERSCOTCH_WIIU_DRC_SPLASH}
+ ICON ${BUTTERSCOTCH_WIIU_ICON}
+ TVSPLASH ${BUTTERSCOTCH_WIIU_TV_SPLASH}
+ CONTENT ${CMAKE_SOURCE_DIR}/resources/wiiu/content
+ )
+
elseif(PLATFORM STREQUAL "ps2")
# The PS2 EE has a single-precision FPU only, so double is software-emulated
add_compile_definitions(USE_FLOAT_REALS)
@@ -332,6 +419,78 @@ elseif(PLATFORM STREQUAL "ps2")
${CMAKE_SOURCE_DIR}/ps2dev-headers/gcc-include
)
endif()
+elseif(PLATFORM STREQUAL "n3ds")
+ option(N3DS_ENABLE_LTO "Enable link-time optimization for the 3DS target" ON)
+ option(N3DS_ENABLE_FAST_MATH "Enable fast-math on the 3DS target" OFF)
+ option(N3DS_USE_FLOAT_REALS "Use float instead of double for GMLReal on the 3DS target" OFF)
+ option(N3DS_DISABLE_RVALUE_INT64 "Store oversized int64 values as reals/int32 on the 3DS target" OFF)
+ option(N3DS_ENABLE_PERF_LOGS "Enable periodic 3DS renderer perf logs" OFF)
+ option(N3DS_DISABLE_BOTTOM_SCREEN "Disable 3DS bottom-screen rendering" OFF)
+
+ if(DEFINED ENV{DEVKITPRO} AND NOT "$ENV{DEVKITPRO}" STREQUAL "")
+ file(TO_CMAKE_PATH "$ENV{DEVKITPRO}" DEVKITPRO_ROOT)
+ elseif(EXISTS "C:/devkitPro")
+ file(TO_CMAKE_PATH "C:/devkitPro" DEVKITPRO_ROOT)
+ else()
+ message(FATAL_ERROR "DEVKITPRO is not set and C:/devkitPro was not found.")
+ endif()
+
+ target_include_directories(butterscotch PRIVATE
+ ${DEVKITPRO_ROOT}/libctru/include
+ ${CMAKE_SOURCE_DIR}/vendor
+ ${CMAKE_SOURCE_DIR}/vendor/stb/ds
+ )
+
+ target_link_directories(butterscotch PRIVATE
+ ${DEVKITPRO_ROOT}/libctru/lib
+ )
+
+ target_compile_options(butterscotch PRIVATE
+ -O3
+ -fomit-frame-pointer
+ -ffunction-sections
+ -fdata-sections
+ $<$:-Os>
+ )
+ target_link_options(butterscotch PRIVATE -Wl,--gc-sections)
+
+ if(N3DS_ENABLE_LTO)
+ target_compile_options(butterscotch PRIVATE -flto)
+ target_link_options(butterscotch PRIVATE -flto)
+ endif()
+ if(N3DS_ENABLE_FAST_MATH)
+ target_compile_options(butterscotch PRIVATE -ffast-math)
+ endif()
+ if(N3DS_USE_FLOAT_REALS)
+ target_compile_definitions(butterscotch PRIVATE USE_FLOAT_REALS)
+ endif()
+ if(N3DS_DISABLE_RVALUE_INT64)
+ target_compile_definitions(butterscotch PRIVATE NO_RVALUE_INT64)
+ endif()
+ if(N3DS_ENABLE_PERF_LOGS)
+ target_compile_definitions(butterscotch PRIVATE N3DS_ENABLE_PERF_LOGS)
+ endif()
+ if(N3DS_DISABLE_BOTTOM_SCREEN)
+ target_compile_definitions(butterscotch PRIVATE N3DS_DISABLE_BOTTOM_SCREEN)
+ endif()
+
+ target_link_libraries(butterscotch PRIVATE citro2d citro3d ctru m)
+ set_target_properties(butterscotch PROPERTIES OUTPUT_NAME cinnamon)
+
+ set(N3DS_ROMFS_DIR "${CMAKE_SOURCE_DIR}/resources/3ds/romfs")
+ set(N3DS_SMDH_FILE "${CMAKE_CURRENT_BINARY_DIR}/cinnamon.default.smdh")
+ ctr_generate_smdh(
+ OUTPUT "${N3DS_SMDH_FILE}"
+ NAME "${BUTTERSCOTCH_3DS_NAME}"
+ DESCRIPTION "${BUTTERSCOTCH_3DS_DESCRIPTION}"
+ AUTHOR "${BUTTERSCOTCH_3DS_AUTHOR}"
+ ICON "${CMAKE_SOURCE_DIR}/resources/3ds/meta/icon.png"
+ )
+ if(EXISTS "${N3DS_ROMFS_DIR}")
+ ctr_create_3dsx(cinnamon TARGET butterscotch SMDH "${N3DS_SMDH_FILE}" ROMFS "${N3DS_ROMFS_DIR}")
+ else()
+ ctr_create_3dsx(cinnamon TARGET butterscotch SMDH "${N3DS_SMDH_FILE}")
+ endif()
else()
message(FATAL_ERROR "Unknown platform! ${PLATFORM}")
endif()
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..6393d099
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,65 @@
+.SUFFIXES:
+
+.DEFAULT_GOAL := help
+
+ROOT_DIR := $(CURDIR)
+export ROOT_DIR
+
+BUILD_3DS := build/n3ds
+TARGET_3DS := cinnamon
+SOURCES_3DS := src src/n3ds
+INCLUDES_3DS := src src/n3ds vendor vendor/stb/ds
+
+BUILD_WIIU := build/wiiu
+
+CMAKE ?= cmake
+ifeq ($(OS),Windows_NT)
+ifneq ($(wildcard /opt/devkitpro/msys2/usr/bin/cmake.exe),)
+CMAKE := /opt/devkitpro/msys2/usr/bin/cmake.exe
+endif
+endif
+
+DEVKITPRO_CMAKE_PATH := $(DEVKITPRO)
+ifeq ($(OS),Windows_NT)
+ifneq ($(strip $(DEVKITPRO)),)
+CYGPATH := $(firstword $(wildcard C:/devkitPro/msys2/usr/bin/cygpath.exe) $(wildcard /usr/bin/cygpath))
+ifneq ($(strip $(CYGPATH)),)
+DEVKITPRO_CMAKE_PATH := $(shell "$(CYGPATH)" -u "$(DEVKITPRO)")
+endif
+endif
+endif
+
+.PHONY: help 3ds 3ds-clean wiiu wiiu-clean
+
+help:
+ @echo "Available targets: 3ds, 3ds-clean, wiiu, wiiu-clean"
+
+wiiu:
+ @"$(CMAKE)" --fresh -S "$(ROOT_DIR)" -B "$(ROOT_DIR)/$(BUILD_WIIU)" -G "Unix Makefiles" \
+ -DCMAKE_TOOLCHAIN_FILE="$(DEVKITPRO_CMAKE_PATH)/cmake/WiiU.cmake" \
+ -DPLATFORM=wiiu \
+ -DCMAKE_BUILD_TYPE=Release
+ @"$(CMAKE)" --build "$(ROOT_DIR)/$(BUILD_WIIU)"
+
+wiiu-clean:
+ @rm -rf "$(ROOT_DIR)/$(BUILD_WIIU)"
+
+THREEDS_GOALS := 3ds
+
+ifneq ($(filter $(THREEDS_GOALS),$(MAKECMDGOALS)),)
+
+ifeq ($(strip $(DEVKITPRO)),)
+$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=")
+endif
+
+3ds:
+ @"$(CMAKE)" --fresh -S "$(ROOT_DIR)" -B "$(ROOT_DIR)/$(BUILD_3DS)" -G "Unix Makefiles" \
+ -DCMAKE_TOOLCHAIN_FILE="$(DEVKITPRO_CMAKE_PATH)/cmake/3DS.cmake" \
+ -DPLATFORM=n3ds \
+ -DCMAKE_BUILD_TYPE=Release
+ @"$(CMAKE)" --build "$(ROOT_DIR)/$(BUILD_3DS)"
+
+endif
+
+3ds-clean:
+ @rm -rf "$(ROOT_DIR)/$(BUILD_3DS)"
diff --git a/README.md b/README.md
index 8236d8f5..d48f053f 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,44 @@
-🥧 Butterscotch 🥧
+Cinnamon
-
-
+
+
+
+
+
+---
> [!IMPORTANT]
-> Butterscotch is still VERY early in development and it is NOT that good yet.
+> Cinnamon is not finished and will have bugs.
When you create a game in GameMaker: Studio and export it, GameMaker: Studio exports the game code as bytecode instead of native compiled code, and that bytecode is compatible with any other GameMaker: Studio runner (also known as YoYo runner), as long as they have matching GameMaker: Studio versions. This is similar to how Java applications work.
-This is how projects such as [Droidtale](https://mrpowergamerbr.com/projects/droidtale) (which was also made by yours truly) can exist. We exploit that GameMaker: Studio games compile to bytecode, which means they can be ran on *any* platform that has an official runner for it!
+This is how projects such as Droidtale can exist. We exploit that GameMaker: Studio games compile to bytecode, which means they can be ran on any platform that has an official runner for it!
+
+If GameMaker games use bytecode, what prevents us from creating our own runner? And if we can write our own runner, what prevents us from porting GameMaker: Studio games to other platforms?
+
+Thats where projects like [Butterscotch](https://github.com/MrPowerGamerBR/Butterscotch) come in! Butterscotch is an open source reimplementation of GameMaker: Studio's runner.
-Ever since I created Droidtale 10+ years ago, I had that lingering thought in my mind... If GameMaker games use bytecode, what prevents us from creating our *own* runner? And if we can write our *own* runner, what prevents us from porting GameMaker: Studio games to other platforms?
+If this already exists, then whats stopping people from porting Butterscotch to MORE consoles? Whats stopping *us* from porting a variety of GameMaker: Studio games to the 3DS and Wii U?
-And that's where Butterscotch comes in! Butterscotch is an open source re-implementation of GameMaker: Studio's runner.
+This is where Cinnamon, a fork of Butterscotch comes in!
+
+Cinnamon aims to be a open source re-implementation of GameMaker Studios runner **for the 3DS and Wii U.** This opens up lots of opportunities for games like Pizza Tower, Undertale Yellow, Undertale, and Deltarune to run on the 3DS and Wii U.
-**Butterscotch PlayStation 2 ISO Generator:** https://butterscotch.mrpowergamerbr.com/
## Game Compatibility
-Butterscotch's goal is to be able to have Undertale v1.08 (GameMaker: Studio 1.4.1804, Bytecode Version 16) fully playable. But we do want to support more GameMaker: Studio games in the future too!
+Butterscotch's and Cinnamon's goal is to be able to have Undertale v1.08 (GameMaker: Studio 1.4.1804, Bytecode Version 16) fully playable. But we do want to support more GameMaker: Studio games in the future too!
-While our target is Undertale v1.08, that doesn't mean that other games CAN'T run in Butterscotch! Because Butterscotch is a runner and not a Undertale port/remake, you CAN run other GameMaker: Studio games with it and, as long as the game is compiled with GameMaker: Studio 1.4.1804 and they only use GML variables and functions that Butterscotch supports, it should work fine.
+While our target is Undertale v1.08, that doesn't mean that other games CAN'T run in Cinnamon! Because Cinnamon itself is a runner and not a Undertale port/remake, you CAN run other GameMaker: Studio games with it and, as long as the game is compiled with GameMaker: Studio 1.4.1804 and they only use GML variables and functions that Cinnamon supports, it should work fine.
-Here are the Bytecode Versions that Butterscotch supports
+Here are the Bytecode Versions that Cinnamon supports
-* Bytecode Version 15
* Bytecode Version 16
* Bytecode Version 17
-However, that doesn't mean that a game that uses a compatible version WILL run! The bytecode support is still a WIP, and Butterscotch may have quirks that the original GameMaker: Studio runner may not have.
+However, that doesn't mean that a game that uses a compatible version WILL run! The bytecode support is still a WIP, and Cinnamon may have quirks that the original GameMaker: Studio runner may not have.
Of course, there are exceptions that break game compatibility altogether:
@@ -38,128 +46,203 @@ Of course, there are exceptions that break game compatibility altogether:
* Games compiled with the new [GMRT](https://github.com/YoYoGames/GMRT-Beta/tree/main), because they use native code instead of bytecode.
## Supported Platforms
-
-* Linux (GLFW, OpenGL)
-* macOS (GLFW, OpenGL)
-* Windows (GLFW, OpenGL, MinGW)
-* PlayStation 2 (ps2sdk, gsKit)
-* Haiku (GLFW)
+* Nintendo 3DS
+* Nintendo Wii U
* ...and maybe more in the future!
-## Community Ports
+## Project Sunshine
+* Project Sunshine is a project that aims to use Cinnamon to port a variety of games (such as UNDERTALE, DELTARUNE, and maybe more games in the future) to the Wii U, 3DS, and maybe more consoles like the GameCube in the future! You can get beta builds on our [Discord](https://discord.gg/AahyBCvVR2) aswell as news on the ports.
+### UNDERTALE: Wii U Edition
+* A released, full port of UNDERTALE on the Wii U. You can download this port on our releases page or on our Discord.
+### DELTARUNE: Wii U Edition
+* A full port of DELTARUNE to the Wii U. Still currently in development. Expect a Chapter 1 release date of before July.
+### UNDERTALE: 3DS Edition
+* A full port of UNDERTALE to the 3DS with 3DS exclusive features such as 3D and bottom screen features.
+### DELTARUNE: 3DS Edition
+* A full port of atleast Chapter One of DELTARUNE to the 3DS.
+
+## Building For Wii U
+
+You must have a proper devkitPro Wii U enviroment set up and configured for your platform. The `wiiu-sdl2` and `ppc-bzip2` devkitPro packages also need to be installed.
+
+On Windows, make sure MinGW is located in your system PATH (C:/MinGW/bin) before proceeding with build instructions.
+
+Configure with the Wii U CMake wrapper and then build:
+
+```bash
+powerpc-eabi-cmake -S . -B build/wiiu -DPLATFORM=wiiu -DCMAKE_BUILD_TYPE=Release
+cmake --build build/wiiu
+```
+
+This produces `Cinnamon.elf`, `Cinnamon.rpx`, and a `.wuhb` bundle in `build/wiiu`.
+
+You can also configure Wii U builds with the toolchain file directly:
+
+```bash
+cmake -S . -B build/wiiu -DPLATFORM=wiiu \
+ -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/WiiU.cmake" \
+ -DCMAKE_BUILD_TYPE=Release
+cmake --build build/wiiu
+```
+
+On Windows, build from PowerShell with:
+
+```powershell
+.\build-windows-wiiu.ps1
+```
+
+## Building for 3DS
+
+You must have a proper devkitPro 3DS environment set up and configured for your platform.
+
+On Windows, make sure MinGW is located in your system PATH (C:/MinGW/bin) before proceeding with build instructions.
+
+Configure and build it with the devkitPro 3DS toolchain:
+
+```bash
+arm-none-eabi-cmake -S . -B build/n3ds -DPLATFORM=n3ds -DCMAKE_BUILD_TYPE=Release
+cmake --build build/n3ds
+```
+
+On Windows, build from PowerShell with:
+
+
+```powershell
+.\build-windows-n3ds.ps1
+```
+
-* [Xbox 360 (Butterscotch-360)](https://github.com/ceilingtilefan/Butterscotch-360) by @ceilingtilefan
-* [3DS and Wii U (Cinnamon)](https://github.com/Project-Sunshine-Native/cinnamon) by @casrielasriel, @grayforz24682, @d16.dorian, @ralcactus
+You can also use the repository Makefile on Linux or from an MSYS2/devkitPro shell on Windows:
-## Building Butterscotch
+```bash
+make 3ds
+```
+
+If you prefer plain CMake, pass the toolchain file explicitly:
```bash
-mkdir build && cd build
-cmake -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Debug ..
-make
+cmake -S . -B build/n3ds -DPLATFORM=n3ds \
+ -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/3DS.cmake" \
+ -DCMAKE_BUILD_TYPE=Release
+cmake --build build/n3ds
```
-If you are using CLion, set the platform in `Settings` > `Build, Execution, Deployment` > `CMake` and add `-DPLATFORM=glfw`
+To build without bottom screen features, pass the disable flag like so:
-Then run Butterscotch with `./butterscotch /path/to/data.win`!
+```bash
+cmake -S . -B build/n3ds -DN3DS_DISABLE_BOTTOM_SCREEN=ON
+```
+
+The main output is `build/n3ds/cinnamon.3dsx`.
-## CLI parameters
+The 3DS build will package `resources/3ds/romfs` into the `.3dsx` if that directory exists. The runner also checks `sdmc:/3ds/cinnamon` at runtime, so you can either bundle preprocessed assets into `romfs` or copy them onto the SD card.
-The GLFW target has a lot of nifty CLI parameters that you can use to trace and debug games running on it.
+## Using the 3DS preprocessor
-* `--debug`: Enables debugging hotkeys
-* `--screenshot=file_%d.png`: Screenshots the runner, requires `--screenshot-at-frame`.
-* `--screenshot-at-frame=Frame`: Screenshots the runner at a specific frame. Can be used multiple times.
-* `--headless`: Runs the runner in headless mode. When running in headless mode, the game will run at the max speed that your system can handle.
-* `--print-rooms`: Prints all the rooms in the `data.win` file and exits.
-* `--print-declared-functions`: Prints all the declared functions (scripts, object events, etc) in the `data.win` file and exists.
-* `--trace-variable-reads`: Traces variable reads
-* `--trace-variable-writes`: Traces variable writes
-* `--trace-function-calls`: Traces function calls
-* `--trace-alarms`: Traces alarms
-* `--trace-instance-lifecycles`: Traces instance creations and deletions
-* `--trace-events`: Traces events
-* `--trace-event-inherited`: Traces event inherited calls
-* `--trace-tiles`: Traces drawn tiles
-* `--trace-opcodes`: Traces opcodes
-* `--trace-stack`: Traces stack
-* `--trace-frames`: Logs when a frame starts and when a frame ends, including how much time it took to process each frame.
-* `--always-log-unknown-functions`: When enabled, Butterscotch will always log unknown functions instead of logging them once per script.
-* `--always-log-stubbed-functions`: When enabled, Butterscotch will always log stubbed functions instead of logging them once per script.
-* `--trace-bytecode-after-frame`: When set, controls when `--trace-opcodes` and `--trace-stack` will start logging. Useful when debugging interpreter-heavy scripts.
-* `--exit-at-frame=Frame`: Automatically exit the runner after X frames.
-* `--speed`: Speed multiplier
-* `--seed=Seed`: Sets a fixed seed for the runner, useful for reproduceable runs.
-* `--print-rooms`: Prints all rooms to the console, along with all objects present in the room.
-* `--print-declared-functions`: Prints all declared GML scripts by the game
-* `--disassemble`: Dissassembles a specific script
-* `--record-inputs`: Records user inputs
-* `--playback-inputs`: Playbacks user inputs
-* `--os-type`: Allows changing the built-in `os_type` value. The default is Windows. Example: When running Undertale Xbox, you would need to set it to `--os-type xboxone`.
-* `--profile-gml-scripts`: Logs which GML scripts are the heaviest in terms of time and executed instructions.
-* `--profile-opcodes`: Ranks which GML opcodes were executed the most.
+The 3DS port expects converted textures and audio instead of the original PC assets. Those files are generated by the standalone `n3ds-preprocess` host tool in `tools/n3ds-preprocess`.
-## Debug Features
+Build the preprocessor with:
-When running Butterscotch with `--debug`, the following hotkeys are enabled:
+```bash
+cmake -S tools/n3ds-preprocess -B build/n3ds-preprocess -DCMAKE_BUILD_TYPE=Release
+cmake --build build/n3ds-preprocess
+```
-* `Page Up`: Moves forward one room
-* `Page Down`: Moves backwards one room
-* `P`: Pauses the game
-* `O`: While paused, advances the game loop by one frame
-* `F12`: Dumps the current runner state to the console
-* `F11`: Dumps the current runner state to the console (JSON format), or dumps it to a file if `--dump-frame-json-file` is set.
-* `F10`: Sets the `global.interact` flag to `0`. Useful in Undertale when you are moving through rooms and one of them starts a cutscene that doesn't let you move.
+The preprocessor uses:
-## Performance
+* `tex3ds` from devkitPro for texture conversion
+* `stb_vorbis` for decoding OGG vorbis audio files for converting to BCWAV 4-bit ADPCM
-Performance is pretty good on any modern computer, but when running on low end targets (like the PS2) it is *very* slow when there's a lot of instances on screen, or when a instance does a for loop.
+On Linux:
+
+* Ensure `tex3ds` is available (usually `/opt/devkitpro/tools/bin/tex3ds`).
+* Run the built binary directly:
+
+```bash
+build/n3ds-preprocess/n3ds-preprocess /path/to/data.win resources/3ds/romfs
+```
+
+If your tools are not in default locations, pass explicit paths:
+
+```bash
+build/n3ds-preprocess/n3ds-preprocess /path/to/data.win resources/3ds/romfs \
+ --tex3ds /opt/devkitpro/tools/bin/tex3ds
+```
+
+On Windows, running `n3ds-preprocess.exe` with no arguments starts an interactive setup that tries to find Undertale automatically and then writes to your SD card layout.
+
+Command line usage:
+
+```bash
+n3ds-preprocess [options]
+```
+
+By default the preprocessor uses hybrid atlas formatting: sprite/background pages stay `rgba5551` for cleaner edges, while safer non-sprite pages may still use `etc1a4`. Use `--texture-format etc1a4`, `--texture-format rgba5551`, or `--page-format-overrides ` if you need to force a specific format.
+
+Useful output directory choices:
+
+* `resources/3ds/romfs` to bundle the generated assets into the next 3DS build
+* Your SD card's `3ds/cinnamon` folder to test assets without rebuilding the `.3dsx`
+
+Example: generate bundled ROMFS assets from Undertale's `data.win`:
+
+```bash
+build/n3ds-preprocess/n3ds-preprocess /path/to/data.win resources/3ds/romfs
+```
+
+Example: write directly to an SD card layout:
+
+```bash
+build/n3ds-preprocess/n3ds-preprocess /path/to/data.win /path/to/SD/3ds/cinnamon
+```
-## Then why not have a transpiler?
+The preprocessor writes:
-The issue with a transpiler is that, if you try transpiling the game in the "naive" way, that is, emitting VM calls like it was the original bytecode, you won't get any
-*improvement* from it, you would need to create a *good* transpiler that actually transpiles it into *good* code, and that's way harder.
+* `gfx/atlas.bin` and converted texture pages to `gfx/`
+* `gfx/direct_assets.bin`, containing packed direct sprite/background/font `.t3x` data with seek metadata
+* SOND data packed directly in `audio/sound_bank.bin`
+* streamed music `.bcwav` files at the output root
-Having a transpiler also have other disadvantages:
+Optional sprite replacements can be placed in a `Sprite_replacements` folder next to the `n3ds-preprocess` executable or in `tools/n3ds-preprocess/Sprite_replacements`. The preprocessor accepts PNGs named by sprite name or sprite index, such as `spr_battlebutton_0.png`, `spr_battlebutton_0_frame_00000.png`, `spr_00042.png`, or `spr_00042_frame_00000.png`. Replacement PNGs must match the logical sprite frame size.
-1. You lose the ability of debugging the runner at a "high level" by tracing opcodes.
-2. Compilation is SLOW, transpiling Undertale in a naive way to C and building it takes 90 seconds on a modern computer, and building it to other targets is so slow that I wasn't even able to test it.
+Optional room border PNGs can be placed in a `Borders` folder next to the `n3ds-preprocess` executable or in `tools/n3ds-preprocess/Borders`. Every top-level `*.png` in that folder is converted to `gfx/borders/.t3x`; useful names include `border_none.png`, `room_ruins.png`, `room_tundra.png`, `room_water.png`, `room_fire.png`, `room_castle.png`, `room_truelab.png`, and `room_gaster.png`.
-## Screenshots
+At runtime, direct textures are loaded from `gfx/direct_assets.bin` when present, with loose-file fallback for overrides/custom files.
+Generated direct sprite/background/font `.t3x` files are removed after packing to keep output size down.
+To reduce SD wear, the preprocessor stages intermediate/unfinalized files in a local temp folder next to the preprocessor executable, then syncs only finalized/changed outputs to your selected destination.
-### Undertale (GLFW) [Bytecode Version 16]
+## Showcase
-
-
-
-
-
-
-
-
-
-
-
-
+### Wii U (Real Hardware)
-### Undertale (PlayStation 2) [Bytecode Version 16]
+- **UNDERTALE (Bytecode 16)**
+
+
+
-Here's a video :3 https://youtu.be/PuzBxe0VGtY
+- **SURVEY_PROGRAM (Bytecode 16)**
+
+
-### DELTARUNE (SURVEY_PROGRAM) (PlayStation 2) [Bytecode Version 16]
+### Wii U (Cemu/Emulator)
-Here's a video :3 https://youtu.be/TLJtV2WnrmQ
+- **SURVEY_PROGRAM (Bytecode 16)**
+
+
-### DELTARUNE Chapter 2 (GLFW) [Bytecode Version 17]
+- **Pizza Tower Demo (Demo 1, Sage 2019 Demo) (Bytecode 16)**
+
+
-
+### 3DS (Real Hardware)
-### DELTARUNE Chapter 3 (GLFW) [Bytecode Version 17]
+- **UNDERTALE (Bytecode 16)**
+
+
+
+
-
-
-
+## Disclaimer
-### DELTARUNE Chapter Selector (GLFW) [Bytecode Version 17]
+Cinnamon has no association, endorsement, or any connection whatsoever with any of the software that it facilitates, and does not provide any of the software it can run by itself. In order to use Cinnamon, you will need to provide your own game files.
-
diff --git a/build-windows-n3ds.ps1 b/build-windows-n3ds.ps1
new file mode 100644
index 00000000..6ef9441a
--- /dev/null
+++ b/build-windows-n3ds.ps1
@@ -0,0 +1,48 @@
+[CmdletBinding()]
+param(
+ [string]$BuildDir = "build\n3ds",
+ [string]$Configuration = "Release",
+ [switch]$NoPause
+)
+
+$ErrorActionPreference = "Stop"
+
+$cmakeExe = "C:\devkitPro\msys2\usr\bin\cmake.exe"
+$msysBin = "C:\devkitPro\msys2\usr\bin"
+$toolchainFile = "/opt/devkitpro/cmake/3DS.cmake"
+
+try {
+ if (-not (Test-Path $cmakeExe)) {
+ throw "devkitPro CMake was not found at '$cmakeExe'. Install devkitPro with the MSYS2 tools first."
+ }
+
+ $env:PATH = "$msysBin;$env:PATH"
+
+ & $cmakeExe --fresh -S . -B $BuildDir -G "Unix Makefiles" `
+ "-DCMAKE_TOOLCHAIN_FILE=$toolchainFile" `
+ -DPLATFORM=n3ds `
+ "-DCMAKE_BUILD_TYPE=$Configuration"
+ if ($LASTEXITCODE -ne 0) {
+ throw "3DS CMake configure failed."
+ }
+
+ & $cmakeExe --build $BuildDir
+ if ($LASTEXITCODE -ne 0) {
+ throw "3DS build failed."
+ }
+
+ Write-Host ""
+ Write-Host "3DS build completed successfully." -ForegroundColor Green
+}
+catch {
+ Write-Host ""
+ Write-Host "3DS build failed." -ForegroundColor Red
+ Write-Host $_
+ exit 1
+}
+finally {
+ if (-not $NoPause) {
+ Write-Host ""
+ Read-Host "Press Enter to close"
+ }
+}
diff --git a/build-windows-wiiu.ps1 b/build-windows-wiiu.ps1
new file mode 100644
index 00000000..10c70ae8
--- /dev/null
+++ b/build-windows-wiiu.ps1
@@ -0,0 +1,48 @@
+[CmdletBinding()]
+param(
+ [string]$BuildDir = "build\wiiu",
+ [string]$Configuration = "Release",
+ [switch]$NoPause
+)
+
+$ErrorActionPreference = "Stop"
+
+$cmakeExe = "C:\devkitPro\msys2\usr\bin\cmake.exe"
+$msysBin = "C:\devkitPro\msys2\usr\bin"
+$toolchainFile = "/opt/devkitpro/cmake/WiiU.cmake"
+
+try {
+ if (-not (Test-Path $cmakeExe)) {
+ throw "devkitPro CMake was not found at '$cmakeExe'. Install devkitPro with the MSYS2 tools first."
+ }
+
+ $env:PATH = "$msysBin;$env:PATH"
+
+ & $cmakeExe --fresh -S . -B $BuildDir -G "Unix Makefiles" `
+ "-DCMAKE_TOOLCHAIN_FILE=$toolchainFile" `
+ -DPLATFORM=wiiu `
+ "-DCMAKE_BUILD_TYPE=$Configuration"
+ if ($LASTEXITCODE -ne 0) {
+ throw "Wii U CMake configure failed."
+ }
+
+ & $cmakeExe --build $BuildDir
+ if ($LASTEXITCODE -ne 0) {
+ throw "Wii U build failed."
+ }
+
+ Write-Host ""
+ Write-Host "Wii U build completed successfully." -ForegroundColor Green
+}
+catch {
+ Write-Host ""
+ Write-Host "Wii U build failed." -ForegroundColor Red
+ Write-Host $_
+ exit 1
+}
+finally {
+ if (-not $NoPause) {
+ Write-Host ""
+ Read-Host "Press Enter to close"
+ }
+}
diff --git a/cmake/StageN3DSRomfs.cmake b/cmake/StageN3DSRomfs.cmake
new file mode 100644
index 00000000..d14829ff
--- /dev/null
+++ b/cmake/StageN3DSRomfs.cmake
@@ -0,0 +1,36 @@
+if(NOT DEFINED SOURCE_DIR OR NOT DEFINED DEST_DIR OR NOT DEFINED STAMP_FILE)
+ message(FATAL_ERROR "SOURCE_DIR, DEST_DIR, and STAMP_FILE must be set.")
+endif()
+
+file(REMOVE_RECURSE "${DEST_DIR}")
+file(MAKE_DIRECTORY "${DEST_DIR}")
+
+file(GLOB_RECURSE ROMFS_FILES
+ RELATIVE "${SOURCE_DIR}"
+ LIST_DIRECTORIES false
+ "${SOURCE_DIR}/*"
+)
+list(SORT ROMFS_FILES)
+
+set(SKIP_PACKED_ATLAS_PAGE_FILES FALSE)
+set(PACKED_ATLAS_PATH "${SOURCE_DIR}/gfx/atlas.bin")
+if(EXISTS "${PACKED_ATLAS_PATH}")
+ file(READ "${PACKED_ATLAS_PATH}" PACKED_ATLAS_VERSION_HEX OFFSET 4 LIMIT 2 HEX)
+ if(PACKED_ATLAS_VERSION_HEX STREQUAL "0800")
+ set(SKIP_PACKED_ATLAS_PAGE_FILES TRUE)
+ endif()
+endif()
+
+foreach(REL_PATH IN LISTS ROMFS_FILES)
+ if(SKIP_PACKED_ATLAS_PAGE_FILES AND REL_PATH MATCHES "^gfx/page_[0-9][0-9][0-9]\\.(t3x|i8)$")
+ continue()
+ endif()
+ set(SRC_PATH "${SOURCE_DIR}/${REL_PATH}")
+ set(DST_PATH "${DEST_DIR}/${REL_PATH}")
+ get_filename_component(DST_PARENT "${DST_PATH}" DIRECTORY)
+ file(MAKE_DIRECTORY "${DST_PARENT}")
+ file(COPY_FILE "${SRC_PATH}" "${DST_PATH}" ONLY_IF_DIFFERENT)
+endforeach()
+
+list(LENGTH ROMFS_FILES ROMFS_FILE_COUNT)
+file(WRITE "${STAMP_FILE}" "staged ${ROMFS_FILE_COUNT} romfs files\n")
diff --git a/icon.png b/icon.png
new file mode 100644
index 00000000..add868b3
Binary files /dev/null and b/icon.png differ
diff --git a/resources/3ds/meta/AppInfo b/resources/3ds/meta/AppInfo
new file mode 100644
index 00000000..4ca8aa39
--- /dev/null
+++ b/resources/3ds/meta/AppInfo
@@ -0,0 +1,9 @@
+APP_TITLE = UNDERTALE
+APP_DESCRIPTION = OG game by Toby Fox.
+APP_AUTHOR = Sunshine Dev Team
+APP_PRODUCT_CODE = 000400000B0E2A00
+APP_UNIQUE_ID = 0x1929
+APP_VERSION_MAJOR = 0
+APP_VERSION_MINOR = 2
+APP_VERSION_MICRO = 0
+APP_SYSTEM_MODE = 0
diff --git a/resources/3ds/meta/audio.mp3 b/resources/3ds/meta/audio.mp3
new file mode 100644
index 00000000..7a2a592b
Binary files /dev/null and b/resources/3ds/meta/audio.mp3 differ
diff --git a/resources/3ds/meta/audio.wav b/resources/3ds/meta/audio.wav
new file mode 100644
index 00000000..b79877c0
Binary files /dev/null and b/resources/3ds/meta/audio.wav differ
diff --git a/resources/3ds/meta/banner.png b/resources/3ds/meta/banner.png
new file mode 100644
index 00000000..3ff6936c
Binary files /dev/null and b/resources/3ds/meta/banner.png differ
diff --git a/resources/3ds/meta/icon.png b/resources/3ds/meta/icon.png
new file mode 100644
index 00000000..ceb070c2
Binary files /dev/null and b/resources/3ds/meta/icon.png differ
diff --git a/resources/3ds/meta/template.rsf b/resources/3ds/meta/template.rsf
new file mode 100644
index 00000000..71d9f7e5
--- /dev/null
+++ b/resources/3ds/meta/template.rsf
@@ -0,0 +1,219 @@
+BasicInfo:
+ Title : $(APP_TITLE)
+ ProductCode : $(APP_PRODUCT_CODE)
+ Logo : Nintendo # Nintendo / Licensed / Distributed / iQue / iQueForSystem
+
+#RomFs:
+ # Specifies the root path of the read only file system to include in the ROM.
+ #RootPath : $(APP_ROMFS)
+
+TitleInfo:
+ Category : Application
+ UniqueId : $(APP_UNIQUE_ID)
+
+Option:
+ UseOnSD : true # true if App is to be installed to SD
+ FreeProductCode : true # Removes limitations on ProductCode
+ MediaFootPadding : false # If true CCI files are created with padding
+ EnableCrypt : $(APP_ENCRYPTED) # Enables encryption for NCCH and CIA
+ EnableCompress : true # Compresses where applicable (currently only exefs:/.code)
+
+AccessControlInfo:
+ CoreVersion : 2
+
+ # Exheader Format Version
+ DescVersion : 2
+
+ # Minimum Required Kernel Version (below is for 4.5.0)
+ ReleaseKernelMajor : "02"
+ ReleaseKernelMinor : "33"
+
+ # ExtData
+ UseExtSaveData : false # enables ExtData
+ #ExtSaveDataId : 0x300 # only set this when the ID is different to the UniqueId
+
+ # FS:USER Archive Access Permissions
+ # Uncomment as required
+ FileSystemAccess:
+ #- CategorySystemApplication
+ #- CategoryHardwareCheck
+ - CategoryFileSystemTool
+ #- Debug
+ #- TwlCardBackup
+ #- TwlNandData
+ #- Boss
+ - DirectSdmc
+ #- Core
+ #- CtrNandRo
+ #- CtrNandRw
+ #- CtrNandRoWrite
+ #- CategorySystemSettings
+ #- CardBoard
+ #- ExportImportIvs
+ - DirectSdmcWrite
+ #- SwitchCleanup
+ #- SaveDataMove
+ #- Shop
+ #- Shell
+ #- CategoryHomeMenu
+
+ # Process Settings
+ MemoryType : Application # Application/System/Base
+ #SystemMode : $(APP_SYSTEM_MODE) # 64MB(Default)/96MB/80MB/72MB/32MB
+ IdealProcessor : 0
+ AffinityMask : 1
+ Priority : 16
+ MaxCpu : 0x9E # Default
+ HandleTableSize : 0x200
+ DisableDebug : false
+ EnableForceDebug : false
+ CanWriteSharedPage : true
+ CanUsePrivilegedPriority : false
+ CanUseNonAlphabetAndNumber : true
+ PermitMainFunctionArgument : true
+ CanShareDeviceMemory : false
+ RunnableOnSleep : false
+ SpecialMemoryArrange : false
+
+ # New3DS Exclusive Process Settings
+ #SystemModeExt : Legacy # Legacy(Default)/124MB/178MB Legacy:Use Old3DS SystemMode
+ #CpuSpeed : 268MHz # 256MHz(Default)/804MHz
+ #EnableL2Cache : false # false(default)/true
+ #CanAccessCore2 : false
+
+ # Virtual Address Mappings
+ #IORegisterMapping:
+ # - 1ff00000-1ff7ffff # DSP memory
+ #MemoryMapping:
+ # - 1f000000-1f5fffff:r # VRAM
+
+ # Accessible SVCs, :
+ SystemCallAccess:
+ ArbitrateAddress: 34
+ #Backdoor: 123
+ Break: 60
+ CancelTimer: 28
+ ClearEvent: 25
+ ClearTimer: 29
+ CloseHandle: 35
+ ConnectToPort: 45
+ ControlMemory: 1
+ ControlProcessMemory: 112
+ CreateAddressArbiter: 33
+ CreateEvent: 23
+ CreateMemoryBlock: 30
+ CreateMutex: 19
+ CreateSemaphore: 21
+ CreateThread: 8
+ CreateTimer: 26
+ DuplicateHandle: 39
+ ExitProcess: 3
+ ExitThread: 9
+ GetCurrentProcessorNumber: 17
+ GetHandleInfo: 41
+ GetProcessId: 53
+ GetProcessIdOfThread: 54
+ GetProcessIdealProcessor: 6
+ GetProcessInfo: 43
+ GetResourceLimit: 56
+ GetResourceLimitCurrentValues: 58
+ GetResourceLimitLimitValues: 57
+ GetSystemInfo: 42
+ GetSystemTick: 40
+ GetThreadContext: 59
+ GetThreadId: 55
+ GetThreadIdealProcessor: 15
+ GetThreadInfo: 44
+ GetThreadPriority: 11
+ MapMemoryBlock: 31
+ OutputDebugString: 61
+ QueryMemory: 2
+ ReleaseMutex: 20
+ ReleaseSemaphore: 22
+ SendSyncRequest1: 46
+ SendSyncRequest2: 47
+ SendSyncRequest3: 48
+ SendSyncRequest4: 49
+ SendSyncRequest: 50
+ SetThreadPriority: 12
+ SetTimer: 27
+ SignalEvent: 24
+ SleepThread: 10
+ UnmapMemoryBlock: 32
+ WaitSynchronization1: 36
+ WaitSynchronizationN: 37
+
+ # Service List
+ # Maximum 34 services (32 if firmware is prior to 9.6.0)
+ ServiceAccessControl:
+ - APT:U
+ #- ac:u
+ #- am:net
+ #- boss:U
+ #- cam:u
+ #- cecd:u
+ #- cfg:nor
+ #- cfg:u
+ #- csnd:SND
+ - dsp::DSP
+ #- frd:u
+ - fs:USER
+ - gsp::Gpu
+ - hid:USER
+ #- http:C
+ #- ir:rst
+ #- ir:u
+ #- ir:USER
+ #- mic:u
+ #- ndm:u
+ #- news:u
+ #- nwm::UDS
+ #- ptm:u
+ #- pxi:dev
+ #- soc:U
+ #- ssl:C
+ #- y2r:u
+
+
+SystemControlInfo:
+ SaveDataSize: 0KB # Change if the app uses savedata
+ RemasterVersion: 2
+ StackSize: 0x40000
+
+ # Modules that run services listed above should be included below
+ # Maximum 48 dependencies
+ # :
+ #Dependency:
+ #ac: 0x0004013000002402
+ #act: 0x0004013000003802
+ #am: 0x0004013000001502
+ #boss: 0x0004013000003402
+ #camera: 0x0004013000001602
+ #cecd: 0x0004013000002602
+ #cfg: 0x0004013000001702
+ #codec: 0x0004013000001802
+ #csnd: 0x0004013000002702
+ #dlp: 0x0004013000002802
+ #dsp: 0x0004013000001a02
+ #friends: 0x0004013000003202
+ #gpio: 0x0004013000001b02
+ # gsp: 0x0004013000001c02
+ # hid: 0x0004013000001d02
+ #http: 0x0004013000002902
+ #i2c: 0x0004013000001e02
+ #ir: 0x0004013000003302
+ #mcu: 0x0004013000001f02
+ #mic: 0x0004013000002002
+ #ndm: 0x0004013000002b02
+ #news: 0x0004013000003502
+ #nfc: 0x0004013000004002
+ #nim: 0x0004013000002c02
+ #nwm: 0x0004013000002d02
+ #pdn: 0x0004013000002102
+ #ps: 0x0004013000003102
+ #ptm: 0x0004013000002202
+ #qtm: 0x0004013020004202
+ #ro: 0x0004013000003702
+ #socket: 0x0004013000002e02
+ #spi: 0x0004013000002302
+ #ssl: 0x0004013000002f02
diff --git a/resources/3ds/source/borders/border_none.png b/resources/3ds/source/borders/border_none.png
new file mode 100644
index 00000000..e36a34ff
Binary files /dev/null and b/resources/3ds/source/borders/border_none.png differ
diff --git a/resources/3ds/source/borders/room_castle.png b/resources/3ds/source/borders/room_castle.png
new file mode 100644
index 00000000..25e35443
Binary files /dev/null and b/resources/3ds/source/borders/room_castle.png differ
diff --git a/resources/3ds/source/borders/room_fire.png b/resources/3ds/source/borders/room_fire.png
new file mode 100644
index 00000000..aa45386c
Binary files /dev/null and b/resources/3ds/source/borders/room_fire.png differ
diff --git a/resources/3ds/source/borders/room_gaster.png b/resources/3ds/source/borders/room_gaster.png
new file mode 100644
index 00000000..9bc1a6c8
Binary files /dev/null and b/resources/3ds/source/borders/room_gaster.png differ
diff --git a/resources/3ds/source/borders/room_ruins.png b/resources/3ds/source/borders/room_ruins.png
new file mode 100644
index 00000000..5137776e
Binary files /dev/null and b/resources/3ds/source/borders/room_ruins.png differ
diff --git a/resources/3ds/source/borders/room_truelab.png b/resources/3ds/source/borders/room_truelab.png
new file mode 100644
index 00000000..710b9719
Binary files /dev/null and b/resources/3ds/source/borders/room_truelab.png differ
diff --git a/resources/3ds/source/borders/room_tundra.png b/resources/3ds/source/borders/room_tundra.png
new file mode 100644
index 00000000..fd6f4f63
Binary files /dev/null and b/resources/3ds/source/borders/room_tundra.png differ
diff --git a/resources/3ds/source/borders/room_water.png b/resources/3ds/source/borders/room_water.png
new file mode 100644
index 00000000..024801c6
Binary files /dev/null and b/resources/3ds/source/borders/room_water.png differ
diff --git a/resources/3ds/textureOverrides/spr_actbt_center_0.png b/resources/3ds/textureOverrides/spr_actbt_center_0.png
new file mode 100644
index 00000000..4e6bbf38
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_actbt_center_1.png b/resources/3ds/textureOverrides/spr_actbt_center_1.png
new file mode 100644
index 00000000..c8490d99
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_actbt_center_hole_0.png b/resources/3ds/textureOverrides/spr_actbt_center_hole_0.png
new file mode 100644
index 00000000..9fb95cbc
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_hole_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_actbt_center_hole_1.png b/resources/3ds/textureOverrides/spr_actbt_center_hole_1.png
new file mode 100644
index 00000000..4e0741a9
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_hole_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_fightbt_0.png b/resources/3ds/textureOverrides/spr_fightbt_0.png
new file mode 100644
index 00000000..a0711e89
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_fightbt_1.png b/resources/3ds/textureOverrides/spr_fightbt_1.png
new file mode 100644
index 00000000..1062648c
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_fightbt_hollow_0.png b/resources/3ds/textureOverrides/spr_fightbt_hollow_0.png
new file mode 100644
index 00000000..4f895d22
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_hollow_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_fightbt_hollow_1.png b/resources/3ds/textureOverrides/spr_fightbt_hollow_1.png
new file mode 100644
index 00000000..1237a2b6
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_hollow_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_itembt_0.png b/resources/3ds/textureOverrides/spr_itembt_0.png
new file mode 100644
index 00000000..e7df72db
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_itembt_1.png b/resources/3ds/textureOverrides/spr_itembt_1.png
new file mode 100644
index 00000000..c80cfc2a
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_itembt_hollow_0.png b/resources/3ds/textureOverrides/spr_itembt_hollow_0.png
new file mode 100644
index 00000000..72a2187c
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_hollow_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_itembt_hollow_1.png b/resources/3ds/textureOverrides/spr_itembt_hollow_1.png
new file mode 100644
index 00000000..fc167885
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_hollow_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_mercybutton_normal_0.png b/resources/3ds/textureOverrides/spr_mercybutton_normal_0.png
new file mode 100644
index 00000000..3468c7e7
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_mercybutton_normal_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_mercybutton_normal_1.png b/resources/3ds/textureOverrides/spr_mercybutton_normal_1.png
new file mode 100644
index 00000000..7b799d9d
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_mercybutton_normal_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_savebt_0.png b/resources/3ds/textureOverrides/spr_savebt_0.png
new file mode 100644
index 00000000..f9421bfa
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_savebt_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_savebt_1.png b/resources/3ds/textureOverrides/spr_savebt_1.png
new file mode 100644
index 00000000..d8fc0e53
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_savebt_1.png differ
diff --git a/resources/3ds/textureOverrides/spr_sparebt_0.png b/resources/3ds/textureOverrides/spr_sparebt_0.png
new file mode 100644
index 00000000..bfb53170
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_sparebt_0.png differ
diff --git a/resources/3ds/textureOverrides/spr_sparebt_1.png b/resources/3ds/textureOverrides/spr_sparebt_1.png
new file mode 100644
index 00000000..4b75cf8f
Binary files /dev/null and b/resources/3ds/textureOverrides/spr_sparebt_1.png differ
diff --git a/resources/readme/screenshots/pic1.jpg b/resources/readme/screenshots/pic1.jpg
new file mode 100644
index 00000000..b96c08bd
Binary files /dev/null and b/resources/readme/screenshots/pic1.jpg differ
diff --git a/resources/wiiu/content/.gitkeep b/resources/wiiu/content/.gitkeep
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/resources/wiiu/content/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/resources/wiiu/content/loadingDog.png b/resources/wiiu/content/loadingDog.png
new file mode 100644
index 00000000..5ba5d926
Binary files /dev/null and b/resources/wiiu/content/loadingDog.png differ
diff --git a/resources/wiiu/meta/drc.png b/resources/wiiu/meta/drc.png
new file mode 100644
index 00000000..3fae179c
Binary files /dev/null and b/resources/wiiu/meta/drc.png differ
diff --git a/resources/wiiu/meta/icon.png b/resources/wiiu/meta/icon.png
new file mode 100644
index 00000000..39d6da90
Binary files /dev/null and b/resources/wiiu/meta/icon.png differ
diff --git a/resources/wiiu/meta/tv.png b/resources/wiiu/meta/tv.png
new file mode 100644
index 00000000..6e929865
Binary files /dev/null and b/resources/wiiu/meta/tv.png differ
diff --git a/resources/wiiu/shaders/pos_col.gsh b/resources/wiiu/shaders/pos_col.gsh
new file mode 100644
index 00000000..e1f2fbf3
Binary files /dev/null and b/resources/wiiu/shaders/pos_col.gsh differ
diff --git a/resources/wiiu/shaders/pos_col.ps b/resources/wiiu/shaders/pos_col.ps
new file mode 100644
index 00000000..152b6254
Binary files /dev/null and b/resources/wiiu/shaders/pos_col.ps differ
diff --git a/resources/wiiu/shaders/pos_col.vs b/resources/wiiu/shaders/pos_col.vs
new file mode 100644
index 00000000..73655a56
--- /dev/null
+++ b/resources/wiiu/shaders/pos_col.vs
@@ -0,0 +1,11 @@
+#version 420 core
+
+layout(location = 0) in vec4 aPosition;
+layout(location = 1) in vec4 aColour;
+
+layout(location = 0) out vec4 vColour;
+
+void main() {
+ gl_Position = aPosition;
+ vColour = aColour;
+}
diff --git a/resources/wiiu/shaders/textured_quad.gsh b/resources/wiiu/shaders/textured_quad.gsh
new file mode 100644
index 00000000..4c93756b
Binary files /dev/null and b/resources/wiiu/shaders/textured_quad.gsh differ
diff --git a/resources/wiiu/shaders/textured_quad.ps b/resources/wiiu/shaders/textured_quad.ps
new file mode 100644
index 00000000..2199765f
Binary files /dev/null and b/resources/wiiu/shaders/textured_quad.ps differ
diff --git a/resources/wiiu/shaders/textured_quad.vs b/resources/wiiu/shaders/textured_quad.vs
new file mode 100644
index 00000000..76c72e4f
--- /dev/null
+++ b/resources/wiiu/shaders/textured_quad.vs
@@ -0,0 +1,14 @@
+#version 420 core
+
+layout(location = 0) in vec4 aPosition;
+layout(location = 1) in vec2 aTexCoord;
+layout(location = 2) in vec4 aColour;
+
+layout(location = 0) out vec2 vTexCoord;
+layout(location = 1) out vec4 vColour;
+
+void main() {
+ gl_Position = aPosition;
+ vTexCoord = aTexCoord;
+ vColour = aColour;
+}
diff --git a/src/audio_system.h b/src/audio_system.h
index 82dcd266..f90e751d 100644
--- a/src/audio_system.h
+++ b/src/audio_system.h
@@ -1,48 +1,50 @@
-#pragma once
-
-#include "common.h"
-#include
-#include
-
-#include "data_win.h"
-#include "file_system.h"
-
-// ===[ AudioSystem Vtable ]===
-
+#pragma once
+
+#include "common.h"
+#include
+#include
+
+#include "data_win.h"
+#include "file_system.h"
+
+// ===[ AudioSystem Vtable ]===
+
typedef struct AudioSystem AudioSystem;
-
-typedef struct {
- void (*init)(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem);
- void (*destroy)(AudioSystem* audio);
- void (*update)(AudioSystem* audio, float deltaTime);
- int32_t (*playSound)(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop);
- void (*stopSound)(AudioSystem* audio, int32_t soundOrInstance);
- void (*stopAll)(AudioSystem* audio);
- bool (*isPlaying)(AudioSystem* audio, int32_t soundOrInstance);
- void (*pauseSound)(AudioSystem* audio, int32_t soundOrInstance);
- void (*resumeSound)(AudioSystem* audio, int32_t soundOrInstance);
- void (*pauseAll)(AudioSystem* audio);
- void (*resumeAll)(AudioSystem* audio);
- void (*setSoundGain)(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs);
- float (*getSoundGain)(AudioSystem* audio, int32_t soundOrInstance);
- void (*setSoundPitch)(AudioSystem* audio, int32_t soundOrInstance, float pitch);
- float (*getSoundPitch)(AudioSystem* audio, int32_t soundOrInstance);
- float (*getTrackPosition)(AudioSystem* audio, int32_t soundOrInstance);
- void (*setTrackPosition)(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds);
- // Total length of a sound in seconds. Accepts either a SOND index or an active sound instance id.
- // Returns 0.0 if unknown (e.g. stream not yet loaded or invalid index).
- float (*getSoundLength)(AudioSystem* audio, int32_t soundOrInstance);
- void (*setMasterGain)(AudioSystem* audio, float gain);
- void (*setChannelCount)(AudioSystem* audio, int32_t count);
+typedef struct Runner Runner;
+
+typedef struct {
+ void (*init)(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem);
+ void (*destroy)(AudioSystem* audio);
+ void (*update)(AudioSystem* audio, float deltaTime);
+ int32_t (*playSound)(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop);
+ void (*stopSound)(AudioSystem* audio, int32_t soundOrInstance);
+ void (*stopAll)(AudioSystem* audio);
+ bool (*isPlaying)(AudioSystem* audio, int32_t soundOrInstance);
+ void (*pauseSound)(AudioSystem* audio, int32_t soundOrInstance);
+ void (*resumeSound)(AudioSystem* audio, int32_t soundOrInstance);
+ void (*pauseAll)(AudioSystem* audio);
+ void (*resumeAll)(AudioSystem* audio);
+ void (*setSoundGain)(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs);
+ float (*getSoundGain)(AudioSystem* audio, int32_t soundOrInstance);
+ void (*setSoundPitch)(AudioSystem* audio, int32_t soundOrInstance, float pitch);
+ float (*getSoundPitch)(AudioSystem* audio, int32_t soundOrInstance);
+ float (*getTrackPosition)(AudioSystem* audio, int32_t soundOrInstance);
+ void (*setTrackPosition)(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds);
+ // Total length of a sound in seconds. Accepts either a SOND index or an active sound instance id.
+ // Returns 0.0 if unknown (e.g. stream not yet loaded or invalid index).
+ float (*getSoundLength)(AudioSystem* audio, int32_t soundOrInstance);
+ void (*setMasterGain)(AudioSystem* audio, float gain);
+ void (*setChannelCount)(AudioSystem* audio, int32_t count);
void (*groupLoad)(AudioSystem* audio, int32_t groupIndex);
bool (*groupIsLoaded)(AudioSystem* audio, int32_t groupIndex);
int32_t (*createStream)(AudioSystem* audio, const char* filename);
bool (*destroyStream)(AudioSystem* audio, int32_t streamIndex);
+ void (*prewarmRoom)(AudioSystem* audio, Runner* runner);
} AudioSystemVtable;
-
-// ===[ AudioSystem Base Struct ]===
-
-struct AudioSystem {
- AudioSystemVtable* vtable;
- DataWin** audioGroups;
-};
+
+// ===[ AudioSystem Base Struct ]===
+
+struct AudioSystem {
+ AudioSystemVtable* vtable;
+ DataWin** audioGroups;
+};
diff --git a/src/binary_reader.c b/src/binary_reader.c
index fd61b999..a18ba179 100644
--- a/src/binary_reader.c
+++ b/src/binary_reader.c
@@ -1,156 +1,156 @@
-#include "binary_reader.h"
-#include "binary_utils.h"
-#include "utils.h"
-
-#include
-#include
-
-BinaryReader BinaryReader_create(FILE* file, size_t fileSize) {
- return (BinaryReader){.file = file, .fileSize = fileSize, .buffer = nullptr, .bufferBase = 0, .bufferSize = 0, .bufferPos = 0};
-}
-
-void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size) {
- reader->buffer = buffer;
- reader->bufferBase = baseOffset;
- reader->bufferSize = size;
- reader->bufferPos = 0;
-}
-
-void BinaryReader_clearBuffer(BinaryReader* reader) {
- reader->buffer = nullptr;
- reader->bufferBase = 0;
- reader->bufferSize = 0;
- reader->bufferPos = 0;
-}
-
-static void readCheck(BinaryReader* reader, void* dest, size_t bytes) {
- if (reader->buffer != nullptr) {
- if (reader->bufferPos + bytes > reader->bufferSize) {
- size_t absPos = reader->bufferBase + reader->bufferPos;
- fprintf(stderr, "BinaryReader: buffer read error at position 0x%zX (requested %zu bytes, buffer has %zu remaining)\n", absPos, bytes, reader->bufferSize - reader->bufferPos);
- abort();
- }
- memcpy(dest, reader->buffer + reader->bufferPos, bytes);
- reader->bufferPos += bytes;
- return;
- }
-
- size_t read = fread(dest, 1, bytes, reader->file);
- if (read != bytes) {
- long pos = ftell(reader->file) - (long) read;
- fprintf(stderr, "BinaryReader: read error at position 0x%lX (requested %zu bytes, got %zu, file size 0x%zX)\n", pos, bytes, read, reader->fileSize);
- abort();
- }
-}
-
-uint8_t BinaryReader_readUint8(BinaryReader* reader) {
- uint8_t value;
- readCheck(reader, &value, 1);
- return value;
-}
-
-int16_t BinaryReader_readInt16(BinaryReader* reader) {
- uint16_t value;
- readCheck(reader, &value, sizeof(value));
- return (int16_t) BinaryUtils_toLittle16(value);
-}
-
-uint16_t BinaryReader_readUint16(BinaryReader* reader) {
- uint16_t value;
- readCheck(reader, &value, sizeof(value));
- return BinaryUtils_toLittle16(value);
-}
-
-int32_t BinaryReader_readInt32(BinaryReader* reader) {
- uint32_t value;
- readCheck(reader, &value, sizeof(value));
- return (int32_t) BinaryUtils_toLittle32(value);
-}
-
-uint32_t BinaryReader_readUint32(BinaryReader* reader) {
- uint32_t value;
- readCheck(reader, &value, sizeof(value));
- return BinaryUtils_toLittle32(value);
-}
-
-float BinaryReader_readFloat32(BinaryReader* reader) {
- uint32_t bits;
- float value;
- readCheck(reader, &bits, sizeof(bits));
- bits = BinaryUtils_toLittle32(bits);
- memcpy(&value, &bits, sizeof(value));
- return value;
-}
-
-uint64_t BinaryReader_readUint64(BinaryReader* reader) {
- uint64_t value;
- readCheck(reader, &value, sizeof(value));
- return BinaryUtils_toLittle64(value);
-}
-
-int64_t BinaryReader_readInt64(BinaryReader* reader) {
- uint64_t value;
- readCheck(reader, &value, sizeof(value));
- return (int64_t) BinaryUtils_toLittle64(value);
-}
-
-bool BinaryReader_readBool32(BinaryReader* reader) {
- return BinaryReader_readUint32(reader) != 0;
-}
-
-void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count) {
- readCheck(reader, dest, count);
-}
-
-uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count) {
- uint8_t* buf = safeMalloc(count);
-
- if (reader->buffer != nullptr) {
- if (offset < reader->bufferBase || offset + count > reader->bufferBase + reader->bufferSize) {
- fprintf(stderr, "BinaryReader: readBytesAt offset 0x%zX+%zu out of buffer range [0x%zX, 0x%zX)\n", offset, count, reader->bufferBase, reader->bufferBase + reader->bufferSize);
- abort();
- }
- size_t savedPos = reader->bufferPos;
- memcpy(buf, reader->buffer + (offset - reader->bufferBase), count);
- reader->bufferPos = savedPos;
- return buf;
- }
-
- long savedPos = ftell(reader->file);
- fseek(reader->file, (long) offset, SEEK_SET);
- readCheck(reader, buf, count);
- fseek(reader->file, savedPos, SEEK_SET);
- return buf;
-}
-
-void BinaryReader_skip(BinaryReader* reader, size_t bytes) {
- if (reader->buffer != nullptr) {
- reader->bufferPos += bytes;
- return;
- }
- fseek(reader->file, (long) bytes, SEEK_CUR);
-}
-
-void BinaryReader_seek(BinaryReader* reader, size_t position) {
- if (reader->buffer != nullptr) {
- if (position < reader->bufferBase || position > reader->bufferBase + reader->bufferSize) {
- fprintf(stderr, "BinaryReader: buffer seek to 0x%zX out of buffer range [0x%zX, 0x%zX]\n", position, reader->bufferBase, reader->bufferBase + reader->bufferSize);
- abort();
- }
- reader->bufferPos = position - reader->bufferBase;
- return;
- }
-
- if (position > reader->fileSize) {
- fprintf(stderr, "BinaryReader: seek to 0x%zX out of bounds (file size 0x%zX)\n", position, reader->fileSize);
- abort();
- }
- fseek(reader->file, (long) position, SEEK_SET);
-}
-
-size_t BinaryReader_getPosition(BinaryReader* reader) {
- if (reader->buffer != nullptr) {
- return reader->bufferBase + reader->bufferPos;
- }
- return (size_t) ftell(reader->file);
-}
+#include "binary_reader.h"
+#include "binary_utils.h"
+#include "utils.h"
+
+#include
+#include
+
+BinaryReader BinaryReader_create(FILE* file, size_t fileSize) {
+ return (BinaryReader){.file = file, .fileSize = fileSize, .buffer = nullptr, .bufferBase = 0, .bufferSize = 0, .bufferPos = 0};
+}
+
+void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size) {
+ reader->buffer = buffer;
+ reader->bufferBase = baseOffset;
+ reader->bufferSize = size;
+ reader->bufferPos = 0;
+}
+
+void BinaryReader_clearBuffer(BinaryReader* reader) {
+ reader->buffer = nullptr;
+ reader->bufferBase = 0;
+ reader->bufferSize = 0;
+ reader->bufferPos = 0;
+}
+
+static void readCheck(BinaryReader* reader, void* dest, size_t bytes) {
+ if (reader->buffer != nullptr) {
+ if (reader->bufferPos + bytes > reader->bufferSize) {
+ size_t absPos = reader->bufferBase + reader->bufferPos;
+ fprintf(stderr, "BinaryReader: buffer read error at position 0x%zX (requested %zu bytes, buffer has %zu remaining)\n", absPos, bytes, reader->bufferSize - reader->bufferPos);
+ abort();
+ }
+ memcpy(dest, reader->buffer + reader->bufferPos, bytes);
+ reader->bufferPos += bytes;
+ return;
+ }
+
+ size_t read = fread(dest, 1, bytes, reader->file);
+ if (read != bytes) {
+ long pos = ftell(reader->file) - (long) read;
+ fprintf(stderr, "BinaryReader: read error at position 0x%lX (requested %zu bytes, got %zu, file size 0x%zX)\n", pos, bytes, read, reader->fileSize);
+ abort();
+ }
+}
+
+uint8_t BinaryReader_readUint8(BinaryReader* reader) {
+ uint8_t value;
+ readCheck(reader, &value, 1);
+ return value;
+}
+
+int16_t BinaryReader_readInt16(BinaryReader* reader) {
+ uint16_t value;
+ readCheck(reader, &value, sizeof(value));
+ return (int16_t) BinaryUtils_toLittle16(value);
+}
+
+uint16_t BinaryReader_readUint16(BinaryReader* reader) {
+ uint16_t value;
+ readCheck(reader, &value, sizeof(value));
+ return BinaryUtils_toLittle16(value);
+}
+
+int32_t BinaryReader_readInt32(BinaryReader* reader) {
+ uint32_t value;
+ readCheck(reader, &value, sizeof(value));
+ return (int32_t) BinaryUtils_toLittle32(value);
+}
+
+uint32_t BinaryReader_readUint32(BinaryReader* reader) {
+ uint32_t value;
+ readCheck(reader, &value, sizeof(value));
+ return BinaryUtils_toLittle32(value);
+}
+
+float BinaryReader_readFloat32(BinaryReader* reader) {
+ uint32_t bits;
+ float value;
+ readCheck(reader, &bits, sizeof(bits));
+ bits = BinaryUtils_toLittle32(bits);
+ memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
+uint64_t BinaryReader_readUint64(BinaryReader* reader) {
+ uint64_t value;
+ readCheck(reader, &value, sizeof(value));
+ return BinaryUtils_toLittle64(value);
+}
+
+int64_t BinaryReader_readInt64(BinaryReader* reader) {
+ uint64_t value;
+ readCheck(reader, &value, sizeof(value));
+ return (int64_t) BinaryUtils_toLittle64(value);
+}
+
+bool BinaryReader_readBool32(BinaryReader* reader) {
+ return BinaryReader_readUint32(reader) != 0;
+}
+
+void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count) {
+ readCheck(reader, dest, count);
+}
+
+uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count) {
+ uint8_t* buf = safeMalloc(count);
+
+ if (reader->buffer != nullptr) {
+ if (offset < reader->bufferBase || offset + count > reader->bufferBase + reader->bufferSize) {
+ fprintf(stderr, "BinaryReader: readBytesAt offset 0x%zX+%zu out of buffer range [0x%zX, 0x%zX)\n", offset, count, reader->bufferBase, reader->bufferBase + reader->bufferSize);
+ abort();
+ }
+ size_t savedPos = reader->bufferPos;
+ memcpy(buf, reader->buffer + (offset - reader->bufferBase), count);
+ reader->bufferPos = savedPos;
+ return buf;
+ }
+
+ long savedPos = ftell(reader->file);
+ fseek(reader->file, (long) offset, SEEK_SET);
+ readCheck(reader, buf, count);
+ fseek(reader->file, savedPos, SEEK_SET);
+ return buf;
+}
+
+void BinaryReader_skip(BinaryReader* reader, size_t bytes) {
+ if (reader->buffer != nullptr) {
+ reader->bufferPos += bytes;
+ return;
+ }
+ fseek(reader->file, (long) bytes, SEEK_CUR);
+}
+
+void BinaryReader_seek(BinaryReader* reader, size_t position) {
+ if (reader->buffer != nullptr) {
+ if (position < reader->bufferBase || position > reader->bufferBase + reader->bufferSize) {
+ fprintf(stderr, "BinaryReader: buffer seek to 0x%zX out of buffer range [0x%zX, 0x%zX]\n", position, reader->bufferBase, reader->bufferBase + reader->bufferSize);
+ abort();
+ }
+ reader->bufferPos = position - reader->bufferBase;
+ return;
+ }
+
+ if (position > reader->fileSize) {
+ fprintf(stderr, "BinaryReader: seek to 0x%zX out of bounds (file size 0x%zX)\n", position, reader->fileSize);
+ abort();
+ }
+ fseek(reader->file, (long) position, SEEK_SET);
+}
+
+size_t BinaryReader_getPosition(BinaryReader* reader) {
+ if (reader->buffer != nullptr) {
+ return reader->bufferBase + reader->bufferPos;
+ }
+ return (size_t) ftell(reader->file);
+}
diff --git a/src/binary_reader.h b/src/binary_reader.h
index e61f6e0e..bdf44426 100644
--- a/src/binary_reader.h
+++ b/src/binary_reader.h
@@ -1,50 +1,50 @@
-#pragma once
-
-#include "common.h"
-#include
-#include
-#include
-#include
-
-typedef struct {
- FILE* file;
- size_t fileSize;
-
- // When non-null, reads come from this memory buffer instead of the FILE*
- // bufferBase is the absolute file offset that buffer[0] corresponds to
- uint8_t* buffer;
- size_t bufferBase;
- size_t bufferSize;
- size_t bufferPos; // current read position relative to bufferBase
-} BinaryReader;
-
-BinaryReader BinaryReader_create(FILE* file, size_t fileSize);
-
-// Sets a memory buffer for bulk chunk reading
-// All subsequent reads will come from this buffer until it is cleared
-// baseOffset is the absolute file offset that buffer[0] corresponds to
-void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size);
-
-// Clears the memory buffer, reverting to FILE*-based reads
-void BinaryReader_clearBuffer(BinaryReader* reader);
-
-uint8_t BinaryReader_readUint8(BinaryReader* reader);
-int16_t BinaryReader_readInt16(BinaryReader* reader);
-uint16_t BinaryReader_readUint16(BinaryReader* reader);
-int32_t BinaryReader_readInt32(BinaryReader* reader);
-uint32_t BinaryReader_readUint32(BinaryReader* reader);
-float BinaryReader_readFloat32(BinaryReader* reader);
-uint64_t BinaryReader_readUint64(BinaryReader* reader);
-int64_t BinaryReader_readInt64(BinaryReader* reader);
-bool BinaryReader_readBool32(BinaryReader* reader);
-
-// Copies 'count' bytes from the current position into 'dest'.
-void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count);
-
-// Reads 'count' bytes from position 'offset' into a newly allocated buffer.
-// Caller must free the returned buffer.
-uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count);
-
-void BinaryReader_skip(BinaryReader* reader, size_t bytes);
-void BinaryReader_seek(BinaryReader* reader, size_t position);
-size_t BinaryReader_getPosition(BinaryReader* reader);
+#pragma once
+
+#include "common.h"
+#include
+#include
+#include
+#include
+
+typedef struct {
+ FILE* file;
+ size_t fileSize;
+
+ // When non-null, reads come from this memory buffer instead of the FILE*
+ // bufferBase is the absolute file offset that buffer[0] corresponds to
+ uint8_t* buffer;
+ size_t bufferBase;
+ size_t bufferSize;
+ size_t bufferPos; // current read position relative to bufferBase
+} BinaryReader;
+
+BinaryReader BinaryReader_create(FILE* file, size_t fileSize);
+
+// Sets a memory buffer for bulk chunk reading
+// All subsequent reads will come from this buffer until it is cleared
+// baseOffset is the absolute file offset that buffer[0] corresponds to
+void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size);
+
+// Clears the memory buffer, reverting to FILE*-based reads
+void BinaryReader_clearBuffer(BinaryReader* reader);
+
+uint8_t BinaryReader_readUint8(BinaryReader* reader);
+int16_t BinaryReader_readInt16(BinaryReader* reader);
+uint16_t BinaryReader_readUint16(BinaryReader* reader);
+int32_t BinaryReader_readInt32(BinaryReader* reader);
+uint32_t BinaryReader_readUint32(BinaryReader* reader);
+float BinaryReader_readFloat32(BinaryReader* reader);
+uint64_t BinaryReader_readUint64(BinaryReader* reader);
+int64_t BinaryReader_readInt64(BinaryReader* reader);
+bool BinaryReader_readBool32(BinaryReader* reader);
+
+// Copies 'count' bytes from the current position into 'dest'.
+void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count);
+
+// Reads 'count' bytes from position 'offset' into a newly allocated buffer.
+// Caller must free the returned buffer.
+uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count);
+
+void BinaryReader_skip(BinaryReader* reader, size_t bytes);
+void BinaryReader_seek(BinaryReader* reader, size_t position);
+size_t BinaryReader_getPosition(BinaryReader* reader);
diff --git a/src/binary_utils.h b/src/binary_utils.h
index ca720c20..54542e66 100644
--- a/src/binary_utils.h
+++ b/src/binary_utils.h
@@ -1,202 +1,202 @@
-#pragma once
-
-#include "common.h"
-#include
-#include
-#include
-
-// Binary reads/writes from a raw byte buffer.
-// When IS_BIG_ENDIAN is defined, reads are byte-swapped to interpret serialized little-endian data.
-
-#if defined(__clang__) || defined(__GNUC__)
-static inline uint16_t BinaryUtils_bswap16(uint16_t value) {
- return __builtin_bswap16(value);
-}
-
-static inline uint32_t BinaryUtils_bswap32(uint32_t value) {
- return __builtin_bswap32(value);
-}
-
-static inline uint64_t BinaryUtils_bswap64(uint64_t value) {
- return __builtin_bswap64(value);
-}
-#elif defined(_MSC_VER)
-static inline uint16_t BinaryUtils_bswap16(uint16_t value) {
- return _byteswap_ushort(value);
-}
-
-static inline uint32_t BinaryUtils_bswap32(uint32_t value) {
- return _byteswap_ulong(value);
-}
-
-static inline uint64_t BinaryUtils_bswap64(uint64_t value) {
- return _byteswap_uint64(value);
-}
-#else
-static inline uint16_t BinaryUtils_bswap16(uint16_t value) {
- return (uint16_t) ((value >> 8) | (value << 8));
-}
-
-static inline uint32_t BinaryUtils_bswap32(uint32_t value) {
- return ((value & 0x000000FFu) << 24) |
- ((value & 0x0000FF00u) << 8) |
- ((value & 0x00FF0000u) >> 8) |
- ((value & 0xFF000000u) >> 24);
-}
-
-static inline uint64_t BinaryUtils_bswap64(uint64_t value) {
- return ((value & 0x00000000000000FFull) << 56) |
- ((value & 0x000000000000FF00ull) << 40) |
- ((value & 0x0000000000FF0000ull) << 24) |
- ((value & 0x00000000FF000000ull) << 8) |
- ((value & 0x000000FF00000000ull) >> 8) |
- ((value & 0x0000FF0000000000ull) >> 24) |
- ((value & 0x00FF000000000000ull) >> 40) |
- ((value & 0xFF00000000000000ull) >> 56);
-}
-#endif
-
-static inline uint16_t BinaryUtils_toLittle16(uint16_t value) {
-#if defined(IS_BIG_ENDIAN)
- return BinaryUtils_bswap16(value);
-#else
- return value;
-#endif
-}
-
-static inline uint32_t BinaryUtils_toLittle32(uint32_t value) {
-#if defined(IS_BIG_ENDIAN)
- return BinaryUtils_bswap32(value);
-#else
- return value;
-#endif
-}
-
-static inline uint64_t BinaryUtils_toLittle64(uint64_t value) {
-#if defined(IS_BIG_ENDIAN)
- return BinaryUtils_bswap64(value);
-#else
- return value;
-#endif
-}
-
-static inline uint8_t BinaryUtils_readUint8(const uint8_t* data) {
- return data[0];
-}
-
-static inline uint16_t BinaryUtils_readUint16(const uint8_t* data) {
- uint16_t val;
- memcpy(&val, data, 2);
- return BinaryUtils_toLittle16(val);
-}
-
-static inline int16_t BinaryUtils_readInt16(const uint8_t* data) {
- return (int16_t) BinaryUtils_readUint16(data);
-}
-
-static inline uint32_t BinaryUtils_readUint32(const uint8_t* data) {
- uint32_t val;
- memcpy(&val, data, 4);
- return BinaryUtils_toLittle32(val);
-}
-
-static inline int32_t BinaryUtils_readInt32(const uint8_t* data) {
- return (int32_t) BinaryUtils_readUint32(data);
-}
-
-static inline uint64_t BinaryUtils_readUint64(const uint8_t* data) {
- uint64_t val;
- memcpy(&val, data, 8);
- return BinaryUtils_toLittle64(val);
-}
-
-static inline int64_t BinaryUtils_readInt64(const uint8_t* data) {
- return (int64_t) BinaryUtils_readUint64(data);
-}
-
-static inline float BinaryUtils_readFloat32(const uint8_t* data) {
- uint32_t bits = BinaryUtils_readUint32(data);
- float val;
- memcpy(&val, &bits, 4);
- return val;
-}
-
-static inline double BinaryUtils_readFloat64(const uint8_t* data) {
- uint64_t bits = BinaryUtils_readUint64(data);
- double val;
- memcpy(&val, &bits, 8);
- return val;
-}
-
-static inline void BinaryUtils_writeUint32(uint8_t* data, uint32_t val) {
- val = BinaryUtils_toLittle32(val);
- memcpy(data, &val, 4);
-}
-
-static inline void BinaryUtils_writeUint16(uint8_t* data, uint16_t val) {
- val = BinaryUtils_toLittle16(val);
- memcpy(data, &val, 2);
-}
-
-static inline void BinaryUtils_writeFloat32(uint8_t* data, float val) {
- uint32_t bits;
- memcpy(&bits, &val, 4);
- bits = BinaryUtils_toLittle32(bits);
- memcpy(data, &bits, 4);
-}
-
-static inline void BinaryUtils_writeFloat64(uint8_t* data, double val) {
- uint64_t bits;
- memcpy(&bits, &val, 8);
- bits = BinaryUtils_toLittle64(bits);
- memcpy(data, &bits, 8);
-}
-
-static inline void BinaryUtils_writeUint64(uint8_t* data, uint64_t val) {
- val = BinaryUtils_toLittle64(val);
- memcpy(data, &val, 8);
-}
-
-static inline void BinaryUtils_writeInt64(uint8_t* data, int64_t val) {
- BinaryUtils_writeUint64(data, (uint64_t) val);
-}
-
-// ===[ Aligned reads ]===
-// These trust the caller to supply a pointer with matching natural alignment.
-// Used on the VM dispatch hot path (bytecode instruction / operand fetch) where the bytecode buffer is guaranteed 4-byte aligned.
-
-static inline uint32_t BinaryUtils_readUint32Aligned(const uint8_t* data) {
- uint32_t val;
- memcpy(&val, __builtin_assume_aligned(data, 4), 4);
- return BinaryUtils_toLittle32(val);
-}
-
-static inline int32_t BinaryUtils_readInt32Aligned(const uint8_t* data) {
- return (int32_t) BinaryUtils_readUint32Aligned(data);
-}
-
-static inline int64_t BinaryUtils_readInt64Aligned(const uint8_t* data) {
- // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned.
- uint64_t val;
- memcpy(&val, __builtin_assume_aligned(data, 4), 8);
- return (int64_t) BinaryUtils_toLittle64(val);
-}
-
-static inline float BinaryUtils_readFloat32Aligned(const uint8_t* data) {
- uint32_t bits;
- memcpy(&bits, __builtin_assume_aligned(data, 4), 4);
- bits = BinaryUtils_toLittle32(bits);
- float val;
- memcpy(&val, &bits, 4);
- return val;
-}
-
-static inline double BinaryUtils_readFloat64Aligned(const uint8_t* data) {
- // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned.
- uint64_t bits;
- memcpy(&bits, __builtin_assume_aligned(data, 4), 8);
- bits = BinaryUtils_toLittle64(bits);
- double val;
- memcpy(&val, &bits, 8);
- return val;
-}
+#pragma once
+
+#include "common.h"
+#include
+#include
+#include
+
+// Binary reads/writes from a raw byte buffer.
+// When IS_BIG_ENDIAN is defined, reads are byte-swapped to interpret serialized little-endian data.
+
+#if defined(__clang__) || defined(__GNUC__)
+static inline uint16_t BinaryUtils_bswap16(uint16_t value) {
+ return __builtin_bswap16(value);
+}
+
+static inline uint32_t BinaryUtils_bswap32(uint32_t value) {
+ return __builtin_bswap32(value);
+}
+
+static inline uint64_t BinaryUtils_bswap64(uint64_t value) {
+ return __builtin_bswap64(value);
+}
+#elif defined(_MSC_VER)
+static inline uint16_t BinaryUtils_bswap16(uint16_t value) {
+ return _byteswap_ushort(value);
+}
+
+static inline uint32_t BinaryUtils_bswap32(uint32_t value) {
+ return _byteswap_ulong(value);
+}
+
+static inline uint64_t BinaryUtils_bswap64(uint64_t value) {
+ return _byteswap_uint64(value);
+}
+#else
+static inline uint16_t BinaryUtils_bswap16(uint16_t value) {
+ return (uint16_t) ((value >> 8) | (value << 8));
+}
+
+static inline uint32_t BinaryUtils_bswap32(uint32_t value) {
+ return ((value & 0x000000FFu) << 24) |
+ ((value & 0x0000FF00u) << 8) |
+ ((value & 0x00FF0000u) >> 8) |
+ ((value & 0xFF000000u) >> 24);
+}
+
+static inline uint64_t BinaryUtils_bswap64(uint64_t value) {
+ return ((value & 0x00000000000000FFull) << 56) |
+ ((value & 0x000000000000FF00ull) << 40) |
+ ((value & 0x0000000000FF0000ull) << 24) |
+ ((value & 0x00000000FF000000ull) << 8) |
+ ((value & 0x000000FF00000000ull) >> 8) |
+ ((value & 0x0000FF0000000000ull) >> 24) |
+ ((value & 0x00FF000000000000ull) >> 40) |
+ ((value & 0xFF00000000000000ull) >> 56);
+}
+#endif
+
+static inline uint16_t BinaryUtils_toLittle16(uint16_t value) {
+#if defined(IS_BIG_ENDIAN)
+ return BinaryUtils_bswap16(value);
+#else
+ return value;
+#endif
+}
+
+static inline uint32_t BinaryUtils_toLittle32(uint32_t value) {
+#if defined(IS_BIG_ENDIAN)
+ return BinaryUtils_bswap32(value);
+#else
+ return value;
+#endif
+}
+
+static inline uint64_t BinaryUtils_toLittle64(uint64_t value) {
+#if defined(IS_BIG_ENDIAN)
+ return BinaryUtils_bswap64(value);
+#else
+ return value;
+#endif
+}
+
+static inline uint8_t BinaryUtils_readUint8(const uint8_t* data) {
+ return data[0];
+}
+
+static inline uint16_t BinaryUtils_readUint16(const uint8_t* data) {
+ uint16_t val;
+ memcpy(&val, data, 2);
+ return BinaryUtils_toLittle16(val);
+}
+
+static inline int16_t BinaryUtils_readInt16(const uint8_t* data) {
+ return (int16_t) BinaryUtils_readUint16(data);
+}
+
+static inline uint32_t BinaryUtils_readUint32(const uint8_t* data) {
+ uint32_t val;
+ memcpy(&val, data, 4);
+ return BinaryUtils_toLittle32(val);
+}
+
+static inline int32_t BinaryUtils_readInt32(const uint8_t* data) {
+ return (int32_t) BinaryUtils_readUint32(data);
+}
+
+static inline uint64_t BinaryUtils_readUint64(const uint8_t* data) {
+ uint64_t val;
+ memcpy(&val, data, 8);
+ return BinaryUtils_toLittle64(val);
+}
+
+static inline int64_t BinaryUtils_readInt64(const uint8_t* data) {
+ return (int64_t) BinaryUtils_readUint64(data);
+}
+
+static inline float BinaryUtils_readFloat32(const uint8_t* data) {
+ uint32_t bits = BinaryUtils_readUint32(data);
+ float val;
+ memcpy(&val, &bits, 4);
+ return val;
+}
+
+static inline double BinaryUtils_readFloat64(const uint8_t* data) {
+ uint64_t bits = BinaryUtils_readUint64(data);
+ double val;
+ memcpy(&val, &bits, 8);
+ return val;
+}
+
+static inline void BinaryUtils_writeUint32(uint8_t* data, uint32_t val) {
+ val = BinaryUtils_toLittle32(val);
+ memcpy(data, &val, 4);
+}
+
+static inline void BinaryUtils_writeUint16(uint8_t* data, uint16_t val) {
+ val = BinaryUtils_toLittle16(val);
+ memcpy(data, &val, 2);
+}
+
+static inline void BinaryUtils_writeFloat32(uint8_t* data, float val) {
+ uint32_t bits;
+ memcpy(&bits, &val, 4);
+ bits = BinaryUtils_toLittle32(bits);
+ memcpy(data, &bits, 4);
+}
+
+static inline void BinaryUtils_writeFloat64(uint8_t* data, double val) {
+ uint64_t bits;
+ memcpy(&bits, &val, 8);
+ bits = BinaryUtils_toLittle64(bits);
+ memcpy(data, &bits, 8);
+}
+
+static inline void BinaryUtils_writeUint64(uint8_t* data, uint64_t val) {
+ val = BinaryUtils_toLittle64(val);
+ memcpy(data, &val, 8);
+}
+
+static inline void BinaryUtils_writeInt64(uint8_t* data, int64_t val) {
+ BinaryUtils_writeUint64(data, (uint64_t) val);
+}
+
+// ===[ Aligned reads ]===
+// These trust the caller to supply a pointer with matching natural alignment.
+// Used on the VM dispatch hot path (bytecode instruction / operand fetch) where the bytecode buffer is guaranteed 4-byte aligned.
+
+static inline uint32_t BinaryUtils_readUint32Aligned(const uint8_t* data) {
+ uint32_t val;
+ memcpy(&val, __builtin_assume_aligned(data, 4), 4);
+ return BinaryUtils_toLittle32(val);
+}
+
+static inline int32_t BinaryUtils_readInt32Aligned(const uint8_t* data) {
+ return (int32_t) BinaryUtils_readUint32Aligned(data);
+}
+
+static inline int64_t BinaryUtils_readInt64Aligned(const uint8_t* data) {
+ // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned.
+ uint64_t val;
+ memcpy(&val, __builtin_assume_aligned(data, 4), 8);
+ return (int64_t) BinaryUtils_toLittle64(val);
+}
+
+static inline float BinaryUtils_readFloat32Aligned(const uint8_t* data) {
+ uint32_t bits;
+ memcpy(&bits, __builtin_assume_aligned(data, 4), 4);
+ bits = BinaryUtils_toLittle32(bits);
+ float val;
+ memcpy(&val, &bits, 4);
+ return val;
+}
+
+static inline double BinaryUtils_readFloat64Aligned(const uint8_t* data) {
+ // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned.
+ uint64_t bits;
+ memcpy(&bits, __builtin_assume_aligned(data, 4), 8);
+ bits = BinaryUtils_toLittle64(bits);
+ double val;
+ memcpy(&val, &bits, 8);
+ return val;
+}
diff --git a/src/collision.h b/src/collision.h
index b3294a6f..98620607 100644
--- a/src/collision.h
+++ b/src/collision.h
@@ -1,417 +1,417 @@
-#pragma once
-
-#include "common.h"
-#include "data_win.h"
-#include "instance.h"
-#include "vm.h"
-
-#include
-
-// Checks if an instance matches a collision target.
-// target >= 100000: instance ID (match specific instance)
-// target == INSTANCE_ALL (-3): match any instance
-// target >= 0 && < 100000: object index (match via parent chain)
-static inline bool Collision_matchesTarget(DataWin* dataWin, Instance* inst, int32_t target) {
- if (target >= 100000) return inst->instanceId == target;
- if (target == INSTANCE_ALL) return true;
- return VM_isObjectOrDescendant(dataWin, inst->objectIndex, target);
-}
-
-typedef struct {
- GMLReal left, right, top, bottom;
- bool valid;
-} InstanceBBox;
-
-// Returns the collision sprite for an instance (mask sprite if set, else display sprite)
-static inline Sprite* Collision_getSprite(DataWin* dataWin, Instance* inst) {
- int32_t sprIdx = (inst->maskIndex >= 0) ? inst->maskIndex : inst->spriteIndex;
- if (0 > sprIdx || (uint32_t) sprIdx >= dataWin->sprt.count) return nullptr;
- return &dataWin->sprt.sprites[sprIdx];
-}
-
-// Computes the axis-aligned bounding box for an instance using its collision sprite
-static inline InstanceBBox Collision_computeBBox(DataWin* dataWin, Instance* inst) {
- Sprite* spr = Collision_getSprite(dataWin, inst);
- if (spr == nullptr) return (InstanceBBox){0, 0, 0, 0, false};
-
- GMLReal marginL = (GMLReal) spr->marginLeft;
- GMLReal marginR = (GMLReal) (spr->marginRight + 1);
- GMLReal marginT = (GMLReal) spr->marginTop;
- GMLReal marginB = (GMLReal) (spr->marginBottom + 1);
- GMLReal originX = (GMLReal) spr->originX;
- GMLReal originY = (GMLReal) spr->originY;
-
- if (GMLReal_fabs(inst->imageAngle) > 0.0001) {
- // Compute rotated AABB: transform the 4 corners of the unrotated bbox
- GMLReal rad = inst->imageAngle * M_PI / 180.0;
- GMLReal cs = GMLReal_cos(rad);
- GMLReal sn = GMLReal_sin(rad);
-
- // Local-space corners relative to origin, scaled
- GMLReal lx0 = inst->imageXscale * (marginL - originX);
- GMLReal ly0 = inst->imageYscale * (marginT - originY);
- GMLReal lx1 = inst->imageXscale * (marginR - originX);
- GMLReal ly1 = inst->imageYscale * (marginB - originY);
-
- // Rotate all 4 corners (CW rotation matching renderer's negated angle for Y-down screen coords)
- GMLReal cx[4], cy[4];
- cx[0] = cs * lx0 + sn * ly0; cy[0] = -sn * lx0 + cs * ly0;
- cx[1] = cs * lx1 + sn * ly0; cy[1] = -sn * lx1 + cs * ly0;
- cx[2] = cs * lx0 + sn * ly1; cy[2] = -sn * lx0 + cs * ly1;
- cx[3] = cs * lx1 + sn * ly1; cy[3] = -sn * lx1 + cs * ly1;
-
- GMLReal minX = cx[0], maxX = cx[0], minY = cy[0], maxY = cy[0];
- for (int c = 1; 4 > c; c++) {
- if (minX > cx[c]) minX = cx[c];
- if (cx[c] > maxX) maxX = cx[c];
- if (minY > cy[c]) minY = cy[c];
- if (cy[c] > maxY) maxY = cy[c];
- }
-
- return (InstanceBBox){
- .left = inst->x + minX,
- .right = inst->x + maxX,
- .top = inst->y + minY,
- .bottom = inst->y + maxY,
- .valid = true
- };
- }
-
- // No rotation fast path
- GMLReal left = inst->x + inst->imageXscale * (marginL - originX);
- GMLReal right = inst->x + inst->imageXscale * (marginR - originX);
- GMLReal top = inst->y + inst->imageYscale * (marginT - originY);
- GMLReal bottom = inst->y + inst->imageYscale * (marginB - originY);
-
- // Normalize if negative scale
- if (left > right) { GMLReal tmp = left; left = right; right = tmp; }
- if (top > bottom) { GMLReal tmp = top; top = bottom; bottom = tmp; }
-
- return (InstanceBBox){left, right, top, bottom, true};
-}
-
-static inline bool Collision_hasFrameMasks(Sprite* sprite) {
- return sprite != nullptr && sprite->sepMasks == 1 && sprite->masks != nullptr && sprite->maskCount > 0;
-}
-
-// Oriented bounding box for a sprite-bearing instance.
-// Local rect is the sprite's collision-margin rectangle, scaled by image_xscale/yscale; (cs, sn) is the rotation that takes local-space points to world-space relative to the instance origin (matches Collision_computeBBox: world = inst.pos + (cs*lx + sn*ly, -sn*lx + cs*ly)).
-typedef struct {
- GMLReal x, y; // World position (instance origin).
- GMLReal lx0, lx1; // Local rect X extents (lx0 <= lx1).
- GMLReal ly0, ly1; // Local rect Y extents (ly0 <= ly1).
- GMLReal cs, sn; // cos/sin of imageAngle (in radians).
- bool rotated; // true if abs(imageAngle) > epsilon.
-} InstanceOBB;
-
-static inline InstanceOBB Collision_instanceOBB(Sprite* spr, Instance* inst) {
- InstanceOBB obb;
- obb.x = inst->x;
- obb.y = inst->y;
- GMLReal marginL = (GMLReal) spr->marginLeft;
- GMLReal marginR = (GMLReal) (spr->marginRight + 1);
- GMLReal marginT = (GMLReal) spr->marginTop;
- GMLReal marginB = (GMLReal) (spr->marginBottom + 1);
- GMLReal originX = (GMLReal) spr->originX;
- GMLReal originY = (GMLReal) spr->originY;
- obb.lx0 = inst->imageXscale * (marginL - originX);
- obb.lx1 = inst->imageXscale * (marginR - originX);
- obb.ly0 = inst->imageYscale * (marginT - originY);
- obb.ly1 = inst->imageYscale * (marginB - originY);
- if (obb.lx0 > obb.lx1) { GMLReal t = obb.lx0; obb.lx0 = obb.lx1; obb.lx1 = t; }
- if (obb.ly0 > obb.ly1) { GMLReal t = obb.ly0; obb.ly0 = obb.ly1; obb.ly1 = t; }
- obb.rotated = GMLReal_fabs(inst->imageAngle) > 0.0001;
- if (obb.rotated) {
- GMLReal rad = inst->imageAngle * M_PI / 180.0;
- obb.cs = GMLReal_cos(rad);
- obb.sn = GMLReal_sin(rad);
- } else {
- obb.cs = 1.0;
- obb.sn = 0.0;
- }
- return obb;
-}
-
-// Inverse-transforms a world point into OBB local coordinates.
-static inline void Collision_obbWorldToLocal(const InstanceOBB* obb, GMLReal wx, GMLReal wy, GMLReal* outLx, GMLReal* outLy) {
- GMLReal dx = wx - obb->x;
- GMLReal dy = wy - obb->y;
- *outLx = dx * obb->cs - dy * obb->sn;
- *outLy = dx * obb->sn + dy * obb->cs;
-}
-
-// Returns true iff the OBB needs SAT-style testing rather than AABB. Only sepMasks == 2 sprites that are actually rotated qualify; everything else (axis-aligned, or precise sprites which fall through to per-pixel scans) is handled correctly by AABB.
-static inline bool Collision_obbNeedsSAT(Sprite* spr, Instance* inst) {
- return spr != nullptr && spr->sepMasks == 2 && GMLReal_fabs(inst->imageAngle) > 0.0001;
-}
-
-static inline bool Collision_rectOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) {
- InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
- if (!bbox.valid) return false;
-
- if (rx1 >= bbox.right || bbox.left >= rx2 || ry1 >= bbox.bottom || bbox.top >= ry2) return false;
-
- Sprite* spr = Collision_getSprite(dataWin, inst);
- if (!Collision_obbNeedsSAT(spr, inst)) return true;
-
- // OBB-vs-AABB SAT for sepMasks==2 with rotation. Native uses SeparatingAxisCollisionBox here.
- InstanceOBB obb = Collision_instanceOBB(spr, inst);
-
- // Project the 4 world-rect corners onto the OBB's local axes; if they don't overlap the local rect on either axis, no collision.
- GMLReal corners[4][2] = { {rx1, ry1}, {rx2, ry1}, {rx1, ry2}, {rx2, ry2} };
- GMLReal uMin = 0, uMax = 0, vMin = 0, vMax = 0;
- repeat(4, c) {
- GMLReal pu, pv;
- Collision_obbWorldToLocal(&obb, corners[c][0], corners[c][1], &pu, &pv);
- if (c == 0) { uMin = uMax = pu; vMin = vMax = pv; }
- else {
- if (pu < uMin) uMin = pu; else if (pu > uMax) uMax = pu;
- if (pv < vMin) vMin = pv; else if (pv > vMax) vMax = pv;
- }
- }
- if (uMin >= obb.lx1 || obb.lx0 >= uMax) return false;
- if (vMin >= obb.ly1 || obb.ly0 >= vMax) return false;
- return true;
-}
-
-// Tests whether a world point lies inside the instance's collision rect (margins, rotated/scaled). Cheaper and more correct than Collision_pointInInstance for sepMasks != 1, since point_in_instance bounds-checks against the full sprite texture rather than the bbox margins.
-static inline bool Collision_pointInsideInstanceBox(DataWin* dataWin, Instance* inst, GMLReal px, GMLReal py) {
- InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
- if (!bbox.valid) return false;
- if (bbox.left > px || px >= bbox.right || bbox.top > py || py >= bbox.bottom) return false;
-
- Sprite* spr = Collision_getSprite(dataWin, inst);
- if (!Collision_obbNeedsSAT(spr, inst)) return true;
-
- InstanceOBB obb = Collision_instanceOBB(spr, inst);
- GMLReal lx, ly;
- Collision_obbWorldToLocal(&obb, px, py, &lx, &ly);
- return lx >= obb.lx0 && obb.lx1 > lx && ly >= obb.ly0 && obb.ly1 > ly;
-}
-
-// Circle (cx, cy, radius) vs instance collision rect. Falls back to circle-vs-AABB when the instance isn't a rotated sepMasks==2 sprite.
-static inline bool Collision_circleOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal cx, GMLReal cy, GMLReal radius) {
- InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
- if (!bbox.valid) return false;
- GMLReal rSq = radius * radius;
-
- Sprite* spr = Collision_getSprite(dataWin, inst);
- if (!Collision_obbNeedsSAT(spr, inst)) {
- // Closest point on AABB to circle center.
- GMLReal closestX = cx;
- if (bbox.left > closestX) closestX = bbox.left;
- if (closestX > bbox.right) closestX = bbox.right;
- GMLReal closestY = cy;
- if (bbox.top > closestY) closestY = bbox.top;
- if (closestY > bbox.bottom) closestY = bbox.bottom;
- GMLReal dx = closestX - cx, dy = closestY - cy;
- return dx * dx + dy * dy <= rSq;
- }
-
- // Loose AABB pre-pass.
- GMLReal qx1 = cx - radius, qy1 = cy - radius, qx2 = cx + radius, qy2 = cy + radius;
- if (qx1 >= bbox.right || bbox.left >= qx2 || qy1 >= bbox.bottom || bbox.top >= qy2) return false;
-
- // Transform circle center into OBB local frame; clamp to local rect; squared distance.
- InstanceOBB obb = Collision_instanceOBB(spr, inst);
- GMLReal lx, ly;
- Collision_obbWorldToLocal(&obb, cx, cy, &lx, &ly);
- GMLReal closestX = lx;
- if (obb.lx0 > closestX) closestX = obb.lx0;
- if (closestX > obb.lx1) closestX = obb.lx1;
- GMLReal closestY = ly;
- if (obb.ly0 > closestY) closestY = obb.ly0;
- if (closestY > obb.ly1) closestY = obb.ly1;
- GMLReal dx = closestX - lx, dy = closestY - ly;
- return dx * dx + dy * dy <= rSq;
-}
-
-// Liang-Barsky clip of a parametric line p(t) = p1 + t*(p2-p1), t in [0,1], against an axis-aligned rect [rx1,rx2] x [ry1,ry2]. Returns true if the segment intersects the rect.
-static inline bool Collision_segmentVsAARect(GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) {
- GMLReal tEnter = 0.0, tExit = 1.0;
- GMLReal dx = x2 - x1, dy = y2 - y1;
- GMLReal p[4] = { -dx, dx, -dy, dy };
- GMLReal q[4] = { x1 - rx1, rx2 - x1, y1 - ry1, ry2 - y1 };
- for (int i = 0; 4 > i; i++) {
- if (GMLReal_fabs(p[i]) < 1e-9) {
- if (q[i] < 0) return false;
- continue;
- }
- GMLReal t = q[i] / p[i];
- if (p[i] < 0) {
- if (t > tEnter) tEnter = t;
- } else {
- if (t < tExit) tExit = t;
- }
- if (tEnter > tExit) return false;
- }
- return true;
-}
-
-// Line segment (x1,y1)-(x2,y2) vs instance collision rect.
-static inline bool Collision_lineOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2) {
- InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
- if (!bbox.valid) return false;
-
- Sprite* spr = Collision_getSprite(dataWin, inst);
- if (!Collision_obbNeedsSAT(spr, inst)) {
- return Collision_segmentVsAARect(x1, y1, x2, y2, bbox.left, bbox.top, bbox.right, bbox.bottom);
- }
-
- // For rotated OBB: transform line endpoints into local frame and clip against local rect.
- InstanceOBB obb = Collision_instanceOBB(spr, inst);
- GMLReal lx1, ly1, lx2, ly2;
- Collision_obbWorldToLocal(&obb, x1, y1, &lx1, &ly1);
- Collision_obbWorldToLocal(&obb, x2, y2, &lx2, &ly2);
- return Collision_segmentVsAARect(lx1, ly1, lx2, ly2, obb.lx0, obb.ly0, obb.lx1, obb.ly1);
-}
-
-// Tests if world point (px, py) is inside the given instance's collision shape.
-// The point is inverse-transformed into sprite-local coords (translation, rotation, inverse scale, origin) and bounds-checked against the full sprite texture [0, spr.width) x [0, spr.height).
-// Precise sprites (sepMasks == 1) additionally require the mask bit at the resulting local pixel to be set.
-static inline bool Collision_pointInInstance(Sprite* spr, Instance* inst, GMLReal px, GMLReal py) {
- if (spr == nullptr) return false;
-
- // Reject degenerate scales to avoid divide-by-zero.
- if (0.0001 > GMLReal_fabs(inst->imageXscale)) return false;
- if (0.0001 > GMLReal_fabs(inst->imageYscale)) return false;
-
- // Transform world coords to sprite-local coords
- GMLReal dx = px - inst->x;
- GMLReal dy = py - inst->y;
-
- // Inverse of CW rotation is standard CCW rotation (positive angle)
- if (GMLReal_fabs(inst->imageAngle) > 0.0001) {
- GMLReal rad = inst->imageAngle * M_PI / 180.0;
- GMLReal cs = GMLReal_cos(rad);
- GMLReal sn = GMLReal_sin(rad);
- GMLReal rx = cs * dx - sn * dy;
- GMLReal ry = sn * dx + cs * dy;
- dx = rx;
- dy = ry;
- }
-
- // Inverse scale + add origin
- GMLReal localX = dx / inst->imageXscale + (GMLReal) spr->originX;
- GMLReal localY = dy / inst->imageYscale + (GMLReal) spr->originY;
-
- int32_t ix = (int32_t) localX;
- int32_t iy = (int32_t) localY;
-
- // Bounds check
- if (0 > ix || 0 > iy || ix >= (int32_t) spr->width || iy >= (int32_t) spr->height) return false;
-
- if (Collision_hasFrameMasks(spr)) {
- // Pick mask for current frame
- uint32_t frameIdx = ((uint32_t) inst->imageIndex) % spr->maskCount;
- uint8_t* mask = spr->masks[frameIdx];
- uint32_t bytesPerRow = (spr->width + 7) / 8;
- return (mask[iy * bytesPerRow + (ix >> 3)] & (1 << (7 - (ix & 7)))) != 0;
- }
-
- return true;
-}
-
-// Returns true if the two instances' collision shapes overlap.
-//
-// Matches the native GMS 1.4 runner's flow in FUN_0043fde0:
-// 1. AABB overlap test on the two precomputed bboxes.
-// 2. If neither sprite is precise (sepMasks == 1), the AABB overlap is enough.
-// 3. Otherwise walk the pixel intersection and test BOTH instances on every
-// pixel via Collision_pointInInstance. Both sides get inverse-transformed
-// regardless of whether they're individually precise, so a rotated
-// non-precise sprite collides as an OBB as long as its partner is precise.
-static inline bool Collision_instancesOverlapPrecise(DataWin* dataWin, bool compatMode, Instance* a, Instance* b, InstanceBBox bboxA, InstanceBBox bboxB) {
- // Compute world-space intersection of the two AABBs
- GMLReal iLeft = GMLReal_fmax(bboxA.left, bboxB.left);
- GMLReal iRight = GMLReal_fmin(bboxA.right, bboxB.right);
- GMLReal iTop = GMLReal_fmax(bboxA.top, bboxB.top);
- GMLReal iBottom = GMLReal_fmin(bboxA.bottom, bboxB.bottom);
-
- // AABB overlap test. Native uses identical semantics in both modern and compat for axis-aligned integer-coordinate cases (compat shifts bbox.right/bottom by -1 *and* the test by +1, which cancel).
- if (iLeft >= iRight || iTop >= iBottom) return false;
-
- Sprite* sprA = Collision_getSprite(dataWin, a);
- Sprite* sprB = Collision_getSprite(dataWin, b);
- if (sprA == nullptr || sprB == nullptr) return false;
-
- bool preciseA = Collision_hasFrameMasks(sprA);
- bool preciseB = Collision_hasFrameMasks(sprB);
-
- // Neither sprite precise? Then we need to check if either side is a rotated sepMasks==2 sprite, in which case the loose AABB engulfs space the rotated rect doesn't, and we need OBB-vs-OBB SAT to match native SeparatingAxisCollisionBox.
- if (!preciseA && !preciseB) {
- bool needSatA = Collision_obbNeedsSAT(sprA, a);
- bool needSatB = Collision_obbNeedsSAT(sprB, b);
- if (!needSatA && !needSatB) return true;
-
- InstanceOBB obbA = Collision_instanceOBB(sprA, a);
- InstanceOBB obbB = Collision_instanceOBB(sprB, b);
-
- // Compute the 4 world corners of each OBB. Indices: 0=(lx0,ly0), 1=(lx1,ly0), 2=(lx0,ly1), 3=(lx1,ly1).
- GMLReal ax[4], ay[4], bx[4], by[4];
- GMLReal lxA[2] = {obbA.lx0, obbA.lx1}, lyA[2] = {obbA.ly0, obbA.ly1};
- GMLReal lxB[2] = {obbB.lx0, obbB.lx1}, lyB[2] = {obbB.ly0, obbB.ly1};
- repeat(2, i) {
- repeat(2, j) {
- int k = j * 2 + i;
- ax[k] = obbA.x + obbA.cs * lxA[i] + obbA.sn * lyA[j];
- ay[k] = obbA.y - obbA.sn * lxA[i] + obbA.cs * lyA[j];
- bx[k] = obbB.x + obbB.cs * lxB[i] + obbB.sn * lyB[j];
- by[k] = obbB.y - obbB.sn * lxB[i] + obbB.cs * lyB[j];
- }
- }
-
- // SAT: 4 axes (each OBB has 2 unique edge normals). axes[0]/[1] are A's local-x/local-y in world space, axes[2]/[3] are B's.
- GMLReal axes[4][2] = {
- { obbA.cs, -obbA.sn },
- { obbA.sn, obbA.cs },
- { obbB.cs, -obbB.sn },
- { obbB.sn, obbB.cs }
- };
- repeat(4, axIdx) {
- GMLReal nx = axes[axIdx][0], ny = axes[axIdx][1];
- GMLReal aMin = ax[0]*nx + ay[0]*ny, aMax = aMin;
- GMLReal bMin = bx[0]*nx + by[0]*ny, bMax = bMin;
- for (int j = 1; 4 > j; j++) {
- GMLReal pa = ax[j]*nx + ay[j]*ny;
- GMLReal pb = bx[j]*nx + by[j]*ny;
- if (pa < aMin) aMin = pa; else if (pa > aMax) aMax = pa;
- if (pb < bMin) bMin = pb; else if (pb > bMax) bMax = pb;
- }
- if (aMax <= bMin || bMax <= aMin) return false;
- }
- return true;
- }
-
- // Pixel scan over the AABB intersection.
- // Modern: floor..ceil with exclusive upper bound, sample pixel centers (+0.5).
- // Compatibility: truncated int range with inclusive upper bound, sample pixel corners (no +0.5).
- int32_t startX, endX, startY, endY;
- GMLReal sampleOffset;
- if (compatMode) {
- startX = (int32_t) iLeft;
- endX = (int32_t) iRight;
- startY = (int32_t) iTop;
- endY = (int32_t) iBottom;
- sampleOffset = 0.0;
- } else {
- startX = (int32_t) GMLReal_floor(iLeft);
- endX = (int32_t) GMLReal_ceil(iRight);
- startY = (int32_t) GMLReal_floor(iTop);
- endY = (int32_t) GMLReal_ceil(iBottom);
- sampleOffset = 0.5;
- }
-
- for (int32_t py = startY; (compatMode ? py <= endY : py < endY); py++) {
- for (int32_t px = startX; (compatMode ? px <= endX : px < endX); px++) {
- GMLReal wpx = (GMLReal) px + sampleOffset;
- GMLReal wpy = (GMLReal) py + sampleOffset;
-
- if (!Collision_pointInInstance(sprA, a, wpx, wpy)) continue;
- if (!Collision_pointInInstance(sprB, b, wpx, wpy)) continue;
- return true;
- }
- }
-
- return false;
-}
+#pragma once
+
+#include "common.h"
+#include "data_win.h"
+#include "instance.h"
+#include "vm.h"
+
+#include
+
+// Checks if an instance matches a collision target.
+// target >= 100000: instance ID (match specific instance)
+// target == INSTANCE_ALL (-3): match any instance
+// target >= 0 && < 100000: object index (match via parent chain)
+static inline bool Collision_matchesTarget(DataWin* dataWin, Instance* inst, int32_t target) {
+ if (target >= 100000) return inst->instanceId == target;
+ if (target == INSTANCE_ALL) return true;
+ return VM_isObjectOrDescendant(dataWin, inst->objectIndex, target);
+}
+
+typedef struct {
+ GMLReal left, right, top, bottom;
+ bool valid;
+} InstanceBBox;
+
+// Returns the collision sprite for an instance (mask sprite if set, else display sprite)
+static inline Sprite* Collision_getSprite(DataWin* dataWin, Instance* inst) {
+ int32_t sprIdx = (inst->maskIndex >= 0) ? inst->maskIndex : inst->spriteIndex;
+ if (0 > sprIdx || (uint32_t) sprIdx >= dataWin->sprt.count) return nullptr;
+ return &dataWin->sprt.sprites[sprIdx];
+}
+
+// Computes the axis-aligned bounding box for an instance using its collision sprite
+static inline InstanceBBox Collision_computeBBox(DataWin* dataWin, Instance* inst) {
+ Sprite* spr = Collision_getSprite(dataWin, inst);
+ if (spr == nullptr) return (InstanceBBox){0, 0, 0, 0, false};
+
+ GMLReal marginL = (GMLReal) spr->marginLeft;
+ GMLReal marginR = (GMLReal) (spr->marginRight + 1);
+ GMLReal marginT = (GMLReal) spr->marginTop;
+ GMLReal marginB = (GMLReal) (spr->marginBottom + 1);
+ GMLReal originX = (GMLReal) spr->originX;
+ GMLReal originY = (GMLReal) spr->originY;
+
+ if (GMLReal_fabs(inst->imageAngle) > 0.0001) {
+ // Compute rotated AABB: transform the 4 corners of the unrotated bbox
+ GMLReal rad = inst->imageAngle * M_PI / 180.0;
+ GMLReal cs = GMLReal_cos(rad);
+ GMLReal sn = GMLReal_sin(rad);
+
+ // Local-space corners relative to origin, scaled
+ GMLReal lx0 = inst->imageXscale * (marginL - originX);
+ GMLReal ly0 = inst->imageYscale * (marginT - originY);
+ GMLReal lx1 = inst->imageXscale * (marginR - originX);
+ GMLReal ly1 = inst->imageYscale * (marginB - originY);
+
+ // Rotate all 4 corners (CW rotation matching renderer's negated angle for Y-down screen coords)
+ GMLReal cx[4], cy[4];
+ cx[0] = cs * lx0 + sn * ly0; cy[0] = -sn * lx0 + cs * ly0;
+ cx[1] = cs * lx1 + sn * ly0; cy[1] = -sn * lx1 + cs * ly0;
+ cx[2] = cs * lx0 + sn * ly1; cy[2] = -sn * lx0 + cs * ly1;
+ cx[3] = cs * lx1 + sn * ly1; cy[3] = -sn * lx1 + cs * ly1;
+
+ GMLReal minX = cx[0], maxX = cx[0], minY = cy[0], maxY = cy[0];
+ for (int c = 1; 4 > c; c++) {
+ if (minX > cx[c]) minX = cx[c];
+ if (cx[c] > maxX) maxX = cx[c];
+ if (minY > cy[c]) minY = cy[c];
+ if (cy[c] > maxY) maxY = cy[c];
+ }
+
+ return (InstanceBBox){
+ .left = inst->x + minX,
+ .right = inst->x + maxX,
+ .top = inst->y + minY,
+ .bottom = inst->y + maxY,
+ .valid = true
+ };
+ }
+
+ // No rotation fast path
+ GMLReal left = inst->x + inst->imageXscale * (marginL - originX);
+ GMLReal right = inst->x + inst->imageXscale * (marginR - originX);
+ GMLReal top = inst->y + inst->imageYscale * (marginT - originY);
+ GMLReal bottom = inst->y + inst->imageYscale * (marginB - originY);
+
+ // Normalize if negative scale
+ if (left > right) { GMLReal tmp = left; left = right; right = tmp; }
+ if (top > bottom) { GMLReal tmp = top; top = bottom; bottom = tmp; }
+
+ return (InstanceBBox){left, right, top, bottom, true};
+}
+
+static inline bool Collision_hasFrameMasks(Sprite* sprite) {
+ return sprite != nullptr && sprite->sepMasks == 1 && sprite->masks != nullptr && sprite->maskCount > 0;
+}
+
+// Oriented bounding box for a sprite-bearing instance.
+// Local rect is the sprite's collision-margin rectangle, scaled by image_xscale/yscale; (cs, sn) is the rotation that takes local-space points to world-space relative to the instance origin (matches Collision_computeBBox: world = inst.pos + (cs*lx + sn*ly, -sn*lx + cs*ly)).
+typedef struct {
+ GMLReal x, y; // World position (instance origin).
+ GMLReal lx0, lx1; // Local rect X extents (lx0 <= lx1).
+ GMLReal ly0, ly1; // Local rect Y extents (ly0 <= ly1).
+ GMLReal cs, sn; // cos/sin of imageAngle (in radians).
+ bool rotated; // true if abs(imageAngle) > epsilon.
+} InstanceOBB;
+
+static inline InstanceOBB Collision_instanceOBB(Sprite* spr, Instance* inst) {
+ InstanceOBB obb;
+ obb.x = inst->x;
+ obb.y = inst->y;
+ GMLReal marginL = (GMLReal) spr->marginLeft;
+ GMLReal marginR = (GMLReal) (spr->marginRight + 1);
+ GMLReal marginT = (GMLReal) spr->marginTop;
+ GMLReal marginB = (GMLReal) (spr->marginBottom + 1);
+ GMLReal originX = (GMLReal) spr->originX;
+ GMLReal originY = (GMLReal) spr->originY;
+ obb.lx0 = inst->imageXscale * (marginL - originX);
+ obb.lx1 = inst->imageXscale * (marginR - originX);
+ obb.ly0 = inst->imageYscale * (marginT - originY);
+ obb.ly1 = inst->imageYscale * (marginB - originY);
+ if (obb.lx0 > obb.lx1) { GMLReal t = obb.lx0; obb.lx0 = obb.lx1; obb.lx1 = t; }
+ if (obb.ly0 > obb.ly1) { GMLReal t = obb.ly0; obb.ly0 = obb.ly1; obb.ly1 = t; }
+ obb.rotated = GMLReal_fabs(inst->imageAngle) > 0.0001;
+ if (obb.rotated) {
+ GMLReal rad = inst->imageAngle * M_PI / 180.0;
+ obb.cs = GMLReal_cos(rad);
+ obb.sn = GMLReal_sin(rad);
+ } else {
+ obb.cs = 1.0;
+ obb.sn = 0.0;
+ }
+ return obb;
+}
+
+// Inverse-transforms a world point into OBB local coordinates.
+static inline void Collision_obbWorldToLocal(const InstanceOBB* obb, GMLReal wx, GMLReal wy, GMLReal* outLx, GMLReal* outLy) {
+ GMLReal dx = wx - obb->x;
+ GMLReal dy = wy - obb->y;
+ *outLx = dx * obb->cs - dy * obb->sn;
+ *outLy = dx * obb->sn + dy * obb->cs;
+}
+
+// Returns true iff the OBB needs SAT-style testing rather than AABB. Only sepMasks == 2 sprites that are actually rotated qualify; everything else (axis-aligned, or precise sprites which fall through to per-pixel scans) is handled correctly by AABB.
+static inline bool Collision_obbNeedsSAT(Sprite* spr, Instance* inst) {
+ return spr != nullptr && spr->sepMasks == 2 && GMLReal_fabs(inst->imageAngle) > 0.0001;
+}
+
+static inline bool Collision_rectOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) {
+ InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
+ if (!bbox.valid) return false;
+
+ if (rx1 >= bbox.right || bbox.left >= rx2 || ry1 >= bbox.bottom || bbox.top >= ry2) return false;
+
+ Sprite* spr = Collision_getSprite(dataWin, inst);
+ if (!Collision_obbNeedsSAT(spr, inst)) return true;
+
+ // OBB-vs-AABB SAT for sepMasks==2 with rotation. Native uses SeparatingAxisCollisionBox here.
+ InstanceOBB obb = Collision_instanceOBB(spr, inst);
+
+ // Project the 4 world-rect corners onto the OBB's local axes; if they don't overlap the local rect on either axis, no collision.
+ GMLReal corners[4][2] = { {rx1, ry1}, {rx2, ry1}, {rx1, ry2}, {rx2, ry2} };
+ GMLReal uMin = 0, uMax = 0, vMin = 0, vMax = 0;
+ repeat(4, c) {
+ GMLReal pu, pv;
+ Collision_obbWorldToLocal(&obb, corners[c][0], corners[c][1], &pu, &pv);
+ if (c == 0) { uMin = uMax = pu; vMin = vMax = pv; }
+ else {
+ if (pu < uMin) uMin = pu; else if (pu > uMax) uMax = pu;
+ if (pv < vMin) vMin = pv; else if (pv > vMax) vMax = pv;
+ }
+ }
+ if (uMin >= obb.lx1 || obb.lx0 >= uMax) return false;
+ if (vMin >= obb.ly1 || obb.ly0 >= vMax) return false;
+ return true;
+}
+
+// Tests whether a world point lies inside the instance's collision rect (margins, rotated/scaled). Cheaper and more correct than Collision_pointInInstance for sepMasks != 1, since point_in_instance bounds-checks against the full sprite texture rather than the bbox margins.
+static inline bool Collision_pointInsideInstanceBox(DataWin* dataWin, Instance* inst, GMLReal px, GMLReal py) {
+ InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
+ if (!bbox.valid) return false;
+ if (bbox.left > px || px >= bbox.right || bbox.top > py || py >= bbox.bottom) return false;
+
+ Sprite* spr = Collision_getSprite(dataWin, inst);
+ if (!Collision_obbNeedsSAT(spr, inst)) return true;
+
+ InstanceOBB obb = Collision_instanceOBB(spr, inst);
+ GMLReal lx, ly;
+ Collision_obbWorldToLocal(&obb, px, py, &lx, &ly);
+ return lx >= obb.lx0 && obb.lx1 > lx && ly >= obb.ly0 && obb.ly1 > ly;
+}
+
+// Circle (cx, cy, radius) vs instance collision rect. Falls back to circle-vs-AABB when the instance isn't a rotated sepMasks==2 sprite.
+static inline bool Collision_circleOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal cx, GMLReal cy, GMLReal radius) {
+ InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
+ if (!bbox.valid) return false;
+ GMLReal rSq = radius * radius;
+
+ Sprite* spr = Collision_getSprite(dataWin, inst);
+ if (!Collision_obbNeedsSAT(spr, inst)) {
+ // Closest point on AABB to circle center.
+ GMLReal closestX = cx;
+ if (bbox.left > closestX) closestX = bbox.left;
+ if (closestX > bbox.right) closestX = bbox.right;
+ GMLReal closestY = cy;
+ if (bbox.top > closestY) closestY = bbox.top;
+ if (closestY > bbox.bottom) closestY = bbox.bottom;
+ GMLReal dx = closestX - cx, dy = closestY - cy;
+ return dx * dx + dy * dy <= rSq;
+ }
+
+ // Loose AABB pre-pass.
+ GMLReal qx1 = cx - radius, qy1 = cy - radius, qx2 = cx + radius, qy2 = cy + radius;
+ if (qx1 >= bbox.right || bbox.left >= qx2 || qy1 >= bbox.bottom || bbox.top >= qy2) return false;
+
+ // Transform circle center into OBB local frame; clamp to local rect; squared distance.
+ InstanceOBB obb = Collision_instanceOBB(spr, inst);
+ GMLReal lx, ly;
+ Collision_obbWorldToLocal(&obb, cx, cy, &lx, &ly);
+ GMLReal closestX = lx;
+ if (obb.lx0 > closestX) closestX = obb.lx0;
+ if (closestX > obb.lx1) closestX = obb.lx1;
+ GMLReal closestY = ly;
+ if (obb.ly0 > closestY) closestY = obb.ly0;
+ if (closestY > obb.ly1) closestY = obb.ly1;
+ GMLReal dx = closestX - lx, dy = closestY - ly;
+ return dx * dx + dy * dy <= rSq;
+}
+
+// Liang-Barsky clip of a parametric line p(t) = p1 + t*(p2-p1), t in [0,1], against an axis-aligned rect [rx1,rx2] x [ry1,ry2]. Returns true if the segment intersects the rect.
+static inline bool Collision_segmentVsAARect(GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) {
+ GMLReal tEnter = 0.0, tExit = 1.0;
+ GMLReal dx = x2 - x1, dy = y2 - y1;
+ GMLReal p[4] = { -dx, dx, -dy, dy };
+ GMLReal q[4] = { x1 - rx1, rx2 - x1, y1 - ry1, ry2 - y1 };
+ for (int i = 0; 4 > i; i++) {
+ if (GMLReal_fabs(p[i]) < 1e-9) {
+ if (q[i] < 0) return false;
+ continue;
+ }
+ GMLReal t = q[i] / p[i];
+ if (p[i] < 0) {
+ if (t > tEnter) tEnter = t;
+ } else {
+ if (t < tExit) tExit = t;
+ }
+ if (tEnter > tExit) return false;
+ }
+ return true;
+}
+
+// Line segment (x1,y1)-(x2,y2) vs instance collision rect.
+static inline bool Collision_lineOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2) {
+ InstanceBBox bbox = Collision_computeBBox(dataWin, inst);
+ if (!bbox.valid) return false;
+
+ Sprite* spr = Collision_getSprite(dataWin, inst);
+ if (!Collision_obbNeedsSAT(spr, inst)) {
+ return Collision_segmentVsAARect(x1, y1, x2, y2, bbox.left, bbox.top, bbox.right, bbox.bottom);
+ }
+
+ // For rotated OBB: transform line endpoints into local frame and clip against local rect.
+ InstanceOBB obb = Collision_instanceOBB(spr, inst);
+ GMLReal lx1, ly1, lx2, ly2;
+ Collision_obbWorldToLocal(&obb, x1, y1, &lx1, &ly1);
+ Collision_obbWorldToLocal(&obb, x2, y2, &lx2, &ly2);
+ return Collision_segmentVsAARect(lx1, ly1, lx2, ly2, obb.lx0, obb.ly0, obb.lx1, obb.ly1);
+}
+
+// Tests if world point (px, py) is inside the given instance's collision shape.
+// The point is inverse-transformed into sprite-local coords (translation, rotation, inverse scale, origin) and bounds-checked against the full sprite texture [0, spr.width) x [0, spr.height).
+// Precise sprites (sepMasks == 1) additionally require the mask bit at the resulting local pixel to be set.
+static inline bool Collision_pointInInstance(Sprite* spr, Instance* inst, GMLReal px, GMLReal py) {
+ if (spr == nullptr) return false;
+
+ // Reject degenerate scales to avoid divide-by-zero.
+ if (0.0001 > GMLReal_fabs(inst->imageXscale)) return false;
+ if (0.0001 > GMLReal_fabs(inst->imageYscale)) return false;
+
+ // Transform world coords to sprite-local coords
+ GMLReal dx = px - inst->x;
+ GMLReal dy = py - inst->y;
+
+ // Inverse of CW rotation is standard CCW rotation (positive angle)
+ if (GMLReal_fabs(inst->imageAngle) > 0.0001) {
+ GMLReal rad = inst->imageAngle * M_PI / 180.0;
+ GMLReal cs = GMLReal_cos(rad);
+ GMLReal sn = GMLReal_sin(rad);
+ GMLReal rx = cs * dx - sn * dy;
+ GMLReal ry = sn * dx + cs * dy;
+ dx = rx;
+ dy = ry;
+ }
+
+ // Inverse scale + add origin
+ GMLReal localX = dx / inst->imageXscale + (GMLReal) spr->originX;
+ GMLReal localY = dy / inst->imageYscale + (GMLReal) spr->originY;
+
+ int32_t ix = (int32_t) localX;
+ int32_t iy = (int32_t) localY;
+
+ // Bounds check
+ if (0 > ix || 0 > iy || ix >= (int32_t) spr->width || iy >= (int32_t) spr->height) return false;
+
+ if (Collision_hasFrameMasks(spr)) {
+ // Pick mask for current frame
+ uint32_t frameIdx = ((uint32_t) inst->imageIndex) % spr->maskCount;
+ uint8_t* mask = spr->masks[frameIdx];
+ uint32_t bytesPerRow = (spr->width + 7) / 8;
+ return (mask[iy * bytesPerRow + (ix >> 3)] & (1 << (7 - (ix & 7)))) != 0;
+ }
+
+ return true;
+}
+
+// Returns true if the two instances' collision shapes overlap.
+//
+// Matches the native GMS 1.4 runner's flow in FUN_0043fde0:
+// 1. AABB overlap test on the two precomputed bboxes.
+// 2. If neither sprite is precise (sepMasks == 1), the AABB overlap is enough.
+// 3. Otherwise walk the pixel intersection and test BOTH instances on every
+// pixel via Collision_pointInInstance. Both sides get inverse-transformed
+// regardless of whether they're individually precise, so a rotated
+// non-precise sprite collides as an OBB as long as its partner is precise.
+static inline bool Collision_instancesOverlapPrecise(DataWin* dataWin, bool compatMode, Instance* a, Instance* b, InstanceBBox bboxA, InstanceBBox bboxB) {
+ // Compute world-space intersection of the two AABBs
+ GMLReal iLeft = GMLReal_fmax(bboxA.left, bboxB.left);
+ GMLReal iRight = GMLReal_fmin(bboxA.right, bboxB.right);
+ GMLReal iTop = GMLReal_fmax(bboxA.top, bboxB.top);
+ GMLReal iBottom = GMLReal_fmin(bboxA.bottom, bboxB.bottom);
+
+ // AABB overlap test. Native uses identical semantics in both modern and compat for axis-aligned integer-coordinate cases (compat shifts bbox.right/bottom by -1 *and* the test by +1, which cancel).
+ if (iLeft >= iRight || iTop >= iBottom) return false;
+
+ Sprite* sprA = Collision_getSprite(dataWin, a);
+ Sprite* sprB = Collision_getSprite(dataWin, b);
+ if (sprA == nullptr || sprB == nullptr) return false;
+
+ bool preciseA = Collision_hasFrameMasks(sprA);
+ bool preciseB = Collision_hasFrameMasks(sprB);
+
+ // Neither sprite precise? Then we need to check if either side is a rotated sepMasks==2 sprite, in which case the loose AABB engulfs space the rotated rect doesn't, and we need OBB-vs-OBB SAT to match native SeparatingAxisCollisionBox.
+ if (!preciseA && !preciseB) {
+ bool needSatA = Collision_obbNeedsSAT(sprA, a);
+ bool needSatB = Collision_obbNeedsSAT(sprB, b);
+ if (!needSatA && !needSatB) return true;
+
+ InstanceOBB obbA = Collision_instanceOBB(sprA, a);
+ InstanceOBB obbB = Collision_instanceOBB(sprB, b);
+
+ // Compute the 4 world corners of each OBB. Indices: 0=(lx0,ly0), 1=(lx1,ly0), 2=(lx0,ly1), 3=(lx1,ly1).
+ GMLReal ax[4], ay[4], bx[4], by[4];
+ GMLReal lxA[2] = {obbA.lx0, obbA.lx1}, lyA[2] = {obbA.ly0, obbA.ly1};
+ GMLReal lxB[2] = {obbB.lx0, obbB.lx1}, lyB[2] = {obbB.ly0, obbB.ly1};
+ repeat(2, i) {
+ repeat(2, j) {
+ int k = j * 2 + i;
+ ax[k] = obbA.x + obbA.cs * lxA[i] + obbA.sn * lyA[j];
+ ay[k] = obbA.y - obbA.sn * lxA[i] + obbA.cs * lyA[j];
+ bx[k] = obbB.x + obbB.cs * lxB[i] + obbB.sn * lyB[j];
+ by[k] = obbB.y - obbB.sn * lxB[i] + obbB.cs * lyB[j];
+ }
+ }
+
+ // SAT: 4 axes (each OBB has 2 unique edge normals). axes[0]/[1] are A's local-x/local-y in world space, axes[2]/[3] are B's.
+ GMLReal axes[4][2] = {
+ { obbA.cs, -obbA.sn },
+ { obbA.sn, obbA.cs },
+ { obbB.cs, -obbB.sn },
+ { obbB.sn, obbB.cs }
+ };
+ repeat(4, axIdx) {
+ GMLReal nx = axes[axIdx][0], ny = axes[axIdx][1];
+ GMLReal aMin = ax[0]*nx + ay[0]*ny, aMax = aMin;
+ GMLReal bMin = bx[0]*nx + by[0]*ny, bMax = bMin;
+ for (int j = 1; 4 > j; j++) {
+ GMLReal pa = ax[j]*nx + ay[j]*ny;
+ GMLReal pb = bx[j]*nx + by[j]*ny;
+ if (pa < aMin) aMin = pa; else if (pa > aMax) aMax = pa;
+ if (pb < bMin) bMin = pb; else if (pb > bMax) bMax = pb;
+ }
+ if (aMax <= bMin || bMax <= aMin) return false;
+ }
+ return true;
+ }
+
+ // Pixel scan over the AABB intersection.
+ // Modern: floor..ceil with exclusive upper bound, sample pixel centers (+0.5).
+ // Compatibility: truncated int range with inclusive upper bound, sample pixel corners (no +0.5).
+ int32_t startX, endX, startY, endY;
+ GMLReal sampleOffset;
+ if (compatMode) {
+ startX = (int32_t) iLeft;
+ endX = (int32_t) iRight;
+ startY = (int32_t) iTop;
+ endY = (int32_t) iBottom;
+ sampleOffset = 0.0;
+ } else {
+ startX = (int32_t) GMLReal_floor(iLeft);
+ endX = (int32_t) GMLReal_ceil(iRight);
+ startY = (int32_t) GMLReal_floor(iTop);
+ endY = (int32_t) GMLReal_ceil(iBottom);
+ sampleOffset = 0.5;
+ }
+
+ for (int32_t py = startY; (compatMode ? py <= endY : py < endY); py++) {
+ for (int32_t px = startX; (compatMode ? px <= endX : px < endX); px++) {
+ GMLReal wpx = (GMLReal) px + sampleOffset;
+ GMLReal wpy = (GMLReal) py + sampleOffset;
+
+ if (!Collision_pointInInstance(sprA, a, wpx, wpy)) continue;
+ if (!Collision_pointInInstance(sprB, b, wpx, wpy)) continue;
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/data_win.c b/src/data_win.c
index ba0e5046..54fe7af6 100644
--- a/src/data_win.c
+++ b/src/data_win.c
@@ -1,2381 +1,2425 @@
-#include "data_win.h"
-#include "binary_reader.h"
-
-#include
-#include
-#include
-#include
-#include
-
-#include "stb_ds.h"
-#include "utils.h"
-
-// ===[ HELPERS ]===
-
-// Reads a uint32 absolute file offset, resolves it into the pre-loaded STRG buffer,
-// and returns a pointer to the null-terminated string content at that offset.
-static const char* readStringPtr(BinaryReader* reader, DataWin* dw) {
- uint32_t offset = BinaryReader_readUint32(reader);
- if (offset == 0) return nullptr;
- return (const char*) (dw->strgBuffer + (offset - dw->strgBufferBase));
-}
-
-// Reads a pointer list header: count + absolute-offset pointers.
-// Caller must free the returned array.
-static uint32_t* readPointerTable(BinaryReader* reader, uint32_t* outCount) {
- *outCount = BinaryReader_readUint32(reader);
- if (*outCount == 0) return nullptr;
- uint32_t* ptrs = safeMalloc(*outCount * sizeof(uint32_t));
- repeat(*outCount, i) {
- ptrs[i] = BinaryReader_readUint32(reader);
- }
- return ptrs;
-}
-
-// Reads a PointerList of EventAction entries. Used by TMLN and OBJT.
-static EventAction* readEventActions(BinaryReader* reader, DataWin* dw, uint32_t* outCount) {
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- *outCount = count;
- if (count == 0) { free(ptrs); return nullptr; }
-
- EventAction* actions = safeMalloc(count * sizeof(EventAction));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- actions[i].libID = BinaryReader_readUint32(reader);
- actions[i].id = BinaryReader_readUint32(reader);
- actions[i].kind = BinaryReader_readUint32(reader);
- actions[i].useRelative = BinaryReader_readBool32(reader);
- actions[i].isQuestion = BinaryReader_readBool32(reader);
- actions[i].useApplyTo = BinaryReader_readBool32(reader);
- actions[i].exeType = BinaryReader_readUint32(reader);
- actions[i].actionName = readStringPtr(reader, dw);
- actions[i].codeId = BinaryReader_readInt32(reader);
- actions[i].argumentCount = BinaryReader_readUint32(reader);
- actions[i].who = BinaryReader_readInt32(reader);
- actions[i].relative = BinaryReader_readBool32(reader);
- actions[i].isNot = BinaryReader_readBool32(reader);
- actions[i].unknownAlwaysZero = BinaryReader_readUint32(reader);
- }
- free(ptrs);
- return actions;
-}
-
-// ===[ PATH INTERNAL COMPUTATION ]===
-// Matches HTML5 yyPath.js algorithm exactly.
-
-// Dynamic array of InternalPathPoints for building during computation
-static InternalPathPoint* tempIntPoints = nullptr;
-static uint32_t tempIntPointCount = 0;
-
-static void addInternalPoint(float x, float y, float speed) {
- InternalPathPoint pt = { .x = x, .y = y, .speed = speed, .l = 0.0 };
- arrput(tempIntPoints, pt);
- tempIntPointCount++;
-}
-
-// Recursive midpoint subdivision for smooth curves (yyPath.js:225-242)
-static void handlePiece(int depth, float x1, float y1, float s1, float x2, float y2, float s2, float x3, float y3, float s3) {
- if (depth == 0) return;
-
- float mx = (x1 + x2 + x2 + x3) / 4.0f;
- float my = (y1 + y2 + y2 + y3) / 4.0f;
- float ms = (s1 + s2 + s2 + s3) / 4.0f;
-
- if ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) > 16.0f) {
- handlePiece(depth - 1, x1, y1, s1, (x2 + x1) / 2.0f, (y2 + y1) / 2.0f, (s2 + s1) / 2.0f, mx, my, ms);
- }
-
- addInternalPoint(mx, my, ms);
-
- if ((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3) > 16.0f) {
- handlePiece(depth - 1, mx, my, ms, (x3 + x2) / 2.0f, (y3 + y2) / 2.0f, (s3 + s2) / 2.0f, x3, y3, s3);
- }
-}
-
-void GamePath_computeInternal(GamePath* path) {
- // Reset temp state
- arrfree(tempIntPoints);
- tempIntPoints = nullptr;
- tempIntPointCount = 0;
-
- free(path->internalPoints);
- path->internalPoints = nullptr;
- path->internalPointCount = 0;
- path->length = 0.0;
-
- if (path->pointCount == 0)
- return;
-
- if (path->isSmooth) {
- // ComputeCurved (yyPath.js:254-292)
- if (!path->isClosed) {
- addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed);
- }
-
- int n;
- if (path->isClosed) {
- n = (int) path->pointCount - 1;
- } else {
- n = (int) path->pointCount - 3;
- }
-
- repeat(n + 1, i) {
- PathPoint* p1 = &path->points[i % path->pointCount];
- PathPoint* p2 = &path->points[(i + 1) % path->pointCount];
- PathPoint* p3 = &path->points[(i + 2) % path->pointCount];
- handlePiece((int) path->precision,
- (p1->x + p2->x) / 2.0f, (p1->y + p2->y) / 2.0f, (p1->speed + p2->speed) / 2.0f,
- p2->x, p2->y, p2->speed,
- (p2->x + p3->x) / 2.0f, (p2->y + p3->y) / 2.0f, (p2->speed + p3->speed) / 2.0f);
- }
-
- if (!path->isClosed) {
- PathPoint* last = &path->points[path->pointCount - 1];
- addInternalPoint(last->x, last->y, last->speed);
- } else {
- // Closed smooth: append the first internal point again
- addInternalPoint(tempIntPoints[0].x, tempIntPoints[0].y, tempIntPoints[0].speed);
- }
- } else {
- // ComputeLinear (yyPath.js:192-204)
- repeat(path->pointCount, i) {
- addInternalPoint(path->points[i].x, path->points[i].y, path->points[i].speed);
- }
- if (path->isClosed) {
- addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed);
- }
- }
-
- // ComputeLength (yyPath.js:150-160)
- path->internalPointCount = tempIntPointCount;
- path->internalPoints = safeMalloc(tempIntPointCount * sizeof(InternalPathPoint));
- memcpy(path->internalPoints, tempIntPoints, tempIntPointCount * sizeof(InternalPathPoint));
- arrfree(tempIntPoints);
- tempIntPoints = nullptr;
- tempIntPointCount = 0;
-
- path->length = 0.0;
- if (path->internalPointCount > 0) {
- path->internalPoints[0].l = 0.0;
- repeat(path->internalPointCount - 1, j) {
- uint32_t i = j + 1;
- float dx = path->internalPoints[i].x - path->internalPoints[i - 1].x;
- float dy = path->internalPoints[i].y - path->internalPoints[i - 1].y;
- path->length += sqrtf(dx * dx + dy * dy);
- path->internalPoints[i].l = path->length;
- }
- }
-}
-
-// Get interpolated position at t in [0,1] (yyPath.js:362-409)
-PathPositionResult GamePath_getPosition(GamePath* path, float t) {
- PathPositionResult result = { .x = 0.0f, .y = 0.0f, .speed = 0.0f };
-
- if (path->internalPointCount == 0) return result;
-
- if (path->internalPointCount == 1 || path->length == 0.0f || 0.0f >= t) {
- result.x = path->internalPoints[0].x;
- result.y = path->internalPoints[0].y;
- result.speed = path->internalPoints[0].speed;
- return result;
- }
-
- if (t >= 1.0f) {
- InternalPathPoint* last = &path->internalPoints[path->internalPointCount - 1];
- result.x = last->x;
- result.y = last->y;
- result.speed = last->speed;
- return result;
- }
-
- // Get the right interval via linear scan
- float l = path->length * t;
- uint32_t pos = 0;
- while (path->internalPointCount - 2 > pos && l >= path->internalPoints[pos + 1].l) {
- pos++;
- }
-
- InternalPathPoint* node = &path->internalPoints[pos];
- float lRem = l - node->l;
- float w = path->internalPoints[pos + 1].l - node->l;
-
- if (w != 0.0f) {
- InternalPathPoint* next = &path->internalPoints[pos + 1];
- result.x = node->x + lRem * (next->x - node->x) / w;
- result.y = node->y + lRem * (next->y - node->y) / w;
- result.speed = node->speed + lRem * (next->speed - node->speed) / w;
- } else {
- result.x = node->x;
- result.y = node->y;
- result.speed = node->speed;
- }
-
- return result;
-}
-
-// ===[ CHUNK PARSERS ]===
-
-static void parseGEN8(BinaryReader* reader, DataWin* dw) {
- Gen8* g = &dw->gen8;
- g->isDebuggerDisabled = BinaryReader_readUint8(reader);
- g->bytecodeVersion = BinaryReader_readUint8(reader);
- BinaryReader_skip(reader, 2); // padding
- g->fileName = readStringPtr(reader, dw);
- g->config = readStringPtr(reader, dw);
- g->lastObj = BinaryReader_readUint32(reader);
- g->lastTile = BinaryReader_readUint32(reader);
- g->gameID = BinaryReader_readUint32(reader);
- BinaryReader_readBytes(reader, g->directPlayGuid, 16);
- g->name = readStringPtr(reader, dw);
- g->major = BinaryReader_readUint32(reader);
- g->minor = BinaryReader_readUint32(reader);
- g->release = BinaryReader_readUint32(reader);
- g->build = BinaryReader_readUint32(reader);
- g->defaultWindowWidth = BinaryReader_readUint32(reader);
- g->defaultWindowHeight = BinaryReader_readUint32(reader);
- g->info = BinaryReader_readUint32(reader);
- g->licenseCRC32 = BinaryReader_readUint32(reader);
- BinaryReader_readBytes(reader, g->licenseMD5, 16);
- g->timestamp = BinaryReader_readUint64(reader);
- g->displayName = readStringPtr(reader, dw);
- g->activeTargets = BinaryReader_readUint64(reader);
- g->functionClassifications = BinaryReader_readUint64(reader);
- g->steamAppID = BinaryReader_readInt32(reader);
- if (g->bytecodeVersion >= 14) {
- g->debuggerPort = BinaryReader_readUint32(reader);
- }
-
- // Room order SimpleList
- g->roomOrderCount = BinaryReader_readUint32(reader);
- if (g->roomOrderCount > 0) {
- g->roomOrder = safeMalloc(g->roomOrderCount * sizeof(int32_t));
- repeat(g->roomOrderCount, i) {
- g->roomOrder[i] = BinaryReader_readInt32(reader);
- }
- } else {
- g->roomOrder = nullptr;
- }
-
- if (g->major >= 2) {
- BinaryReader_skip(reader, 8); // firstRandom (int64)
- BinaryReader_skip(reader, 8*4); // 4 Random Entries (one int64 or two int32)
-
- g->gms2FPS = BinaryReader_readFloat32(reader);
- BinaryReader_skip(reader, 4); // AllowStatistics (bool32)
- BinaryReader_skip(reader, 16); // GameGUID (16 Bytes, unknown it's use)
- }
-
- // Seed the detected version from GEN8.
- // Later chunk parsers may bump these upward when they identify newer-format features, because since GM:S 2 the value in the GEN8 chunk is not accurate.
- DataWin_bumpVersionTo(dw, g->major, g->minor, g->release, g->build);
-}
-
-static void parseOPTN(BinaryReader* reader, DataWin* dw) {
- Optn* o = &dw->optn;
-
- int32_t marker = BinaryReader_readInt32(reader);
- if (marker != (int32_t)0x80000000) {
- fprintf(stderr, "OPTN: expected new format marker 0x80000000, got 0x%08X\n", (uint32_t)marker);
- exit(1);
- }
-
- int32_t shaderExtVersion = BinaryReader_readInt32(reader);
- (void)shaderExtVersion; // always 2
-
- o->info = BinaryReader_readUint64(reader);
- o->scale = BinaryReader_readInt32(reader);
- o->windowColor = BinaryReader_readUint32(reader);
- o->colorDepth = BinaryReader_readUint32(reader);
- o->resolution = BinaryReader_readUint32(reader);
- o->frequency = BinaryReader_readUint32(reader);
- o->vertexSync = BinaryReader_readUint32(reader);
- o->priority = BinaryReader_readUint32(reader);
- o->backImage = BinaryReader_readUint32(reader);
- o->frontImage = BinaryReader_readUint32(reader);
- o->loadImage = BinaryReader_readUint32(reader);
- o->loadAlpha = BinaryReader_readUint32(reader);
-
- // Constants SimpleList
- o->constantCount = BinaryReader_readUint32(reader);
- if (o->constantCount > 0) {
- o->constants = safeMalloc(o->constantCount * sizeof(OptnConstant));
- repeat(o->constantCount, i) {
- o->constants[i].name = readStringPtr(reader, dw);
- o->constants[i].value = readStringPtr(reader, dw);
- }
- } else {
- o->constants = nullptr;
- }
-}
-
-static void parseLANG(BinaryReader* reader, DataWin* dw) {
- Lang* l = &dw->lang;
- l->unknown1 = BinaryReader_readUint32(reader);
- l->languageCount = BinaryReader_readUint32(reader);
- l->entryCount = BinaryReader_readUint32(reader);
-
- // Entry IDs
- if (l->entryCount > 0) {
- l->entryIds = safeMalloc(l->entryCount * sizeof(const char*));
- repeat(l->entryCount, i) {
- l->entryIds[i] = readStringPtr(reader, dw);
- }
- } else {
- l->entryIds = nullptr;
- }
-
- // Languages
- if (l->languageCount > 0) {
- l->languages = safeMalloc(l->languageCount * sizeof(Language));
- repeat(l->languageCount, i) {
- l->languages[i].name = readStringPtr(reader, dw);
- l->languages[i].region = readStringPtr(reader, dw);
- l->languages[i].entryCount = l->entryCount;
- if (l->entryCount > 0) {
- l->languages[i].entries = safeMalloc(l->entryCount * sizeof(const char*));
- repeat(l->entryCount, j) {
- l->languages[i].entries[j] = readStringPtr(reader, dw);
- }
- } else {
- l->languages[i].entries = nullptr;
- }
- }
- } else {
- l->languages = nullptr;
- }
-}
-
-static void parseEXTN(BinaryReader* reader, DataWin* dw) {
- // TODO: Update EXTN parser because it is broken for newer GM:S 2 versions
- Extn* e = &dw->extn;
-
- uint32_t extCount;
- uint32_t* extPtrs = readPointerTable(reader, &extCount);
- e->count = extCount;
-
- if (extCount == 0) { free(extPtrs); e->extensions = nullptr; return; }
-
- e->extensions = safeMalloc(extCount * sizeof(Extension));
- repeat(extCount, i) {
- BinaryReader_seek(reader, extPtrs[i]);
- Extension* ext = &e->extensions[i];
- ext->folderName = readStringPtr(reader, dw);
- ext->name = readStringPtr(reader, dw);
- ext->className = readStringPtr(reader, dw);
-
- // Files PointerList
- uint32_t fileCount;
- uint32_t* filePtrs = readPointerTable(reader, &fileCount);
- ext->fileCount = fileCount;
-
- if (fileCount > 0) {
- ext->files = safeMalloc(fileCount * sizeof(ExtensionFile));
- repeat(fileCount, j) {
- BinaryReader_seek(reader, filePtrs[j]);
- ExtensionFile* file = &ext->files[j];
- file->filename = readStringPtr(reader, dw);
- file->cleanupScript = readStringPtr(reader, dw);
- file->initScript = readStringPtr(reader, dw);
- file->kind = BinaryReader_readUint32(reader);
-
- // Functions PointerList
- uint32_t funcCount;
- uint32_t* funcPtrs = readPointerTable(reader, &funcCount);
- file->functionCount = funcCount;
-
- if (funcCount > 0) {
- file->functions = safeMalloc(funcCount * sizeof(ExtensionFunction));
- repeat(funcCount, k) {
- BinaryReader_seek(reader, funcPtrs[k]);
- ExtensionFunction* func = &file->functions[k];
- func->name = readStringPtr(reader, dw);
- func->id = BinaryReader_readUint32(reader);
- func->kind = BinaryReader_readUint32(reader);
- func->retType = BinaryReader_readUint32(reader);
- func->extName = readStringPtr(reader, dw);
-
- // Arguments SimpleList
- func->argumentCount = BinaryReader_readUint32(reader);
- if (func->argumentCount > 0) {
- func->arguments = safeMalloc(func->argumentCount * sizeof(uint32_t));
- repeat(func->argumentCount, a) {
- func->arguments[a] = BinaryReader_readUint32(reader);
- }
- } else {
- func->arguments = nullptr;
- }
- }
- } else {
- file->functions = nullptr;
- }
- free(funcPtrs);
- }
- } else {
- ext->files = nullptr;
- }
- free(filePtrs);
- }
- free(extPtrs);
-
- // Product ID data (16 bytes per extension, bytecodeVersion >= 14)
- // Skipped -- we seek to chunkEnd after parsing
-}
-
-static void parseSOND(BinaryReader* reader, DataWin* dw) {
- Sond* s = &dw->sond;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- s->count = count;
-
- if (count == 0) { free(ptrs); s->sounds = nullptr; return; }
-
- s->sounds = safeMalloc(count * sizeof(Sound));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- Sound* snd = &s->sounds[i];
- snd->name = readStringPtr(reader, dw);
- snd->flags = BinaryReader_readUint32(reader);
- snd->type = readStringPtr(reader, dw);
- snd->file = readStringPtr(reader, dw);
- snd->effects = BinaryReader_readUint32(reader);
- snd->volume = BinaryReader_readFloat32(reader);
- snd->pitch = BinaryReader_readFloat32(reader);
-
- // AudioGroup or preload field at offset +28
- // For GMS 1.4.x (bytecodeVersion >= 14) with Regular flag: resource_id
- if ((snd->flags & 0x64) == 0x64) {
- snd->audioGroup = BinaryReader_readInt32(reader);
- } else {
- int32_t preload = BinaryReader_readInt32(reader);
- (void)preload;
- snd->audioGroup = 0; // default audio group
- }
-
- snd->audioFile = BinaryReader_readInt32(reader);
- }
- free(ptrs);
-}
-
-static void parseAGRP(BinaryReader* reader, DataWin* dw) {
- Agrp* a = &dw->agrp;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- a->count = count;
-
- if (count == 0) { free(ptrs); a->audioGroups = nullptr; return; }
-
- a->audioGroups = safeMalloc(count * sizeof(AudioGroup));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- a->audioGroups[i].name = readStringPtr(reader, dw);
- }
- free(ptrs);
-}
-
-static void parseSPRT(BinaryReader* reader, DataWin* dw, bool skipLoadingPreciseMasksForNonPreciseSprites) {
- Sprt* s = &dw->sprt;
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- s->count = count;
- s->parsedCount = count;
-
- if (count == 0) { free(ptrs); s->sprites = nullptr; return; }
-
- s->sprites = safeCalloc(count, sizeof(Sprite));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- Sprite* spr = &s->sprites[i];
- spr->name = readStringPtr(reader, dw);
- spr->width = BinaryReader_readUint32(reader);
- spr->height = BinaryReader_readUint32(reader);
- spr->marginLeft = BinaryReader_readInt32(reader);
- spr->marginRight = BinaryReader_readInt32(reader);
- spr->marginBottom = BinaryReader_readInt32(reader);
- spr->marginTop = BinaryReader_readInt32(reader);
- spr->transparent = BinaryReader_readBool32(reader);
- spr->smooth = BinaryReader_readBool32(reader);
- spr->preload = BinaryReader_readBool32(reader);
- spr->bboxMode = BinaryReader_readUint32(reader);
- spr->sepMasks = BinaryReader_readUint32(reader);
- spr->originX = BinaryReader_readInt32(reader);
- spr->originY = BinaryReader_readInt32(reader);
-
- // Detect special type vs normal: peek next int32
- int32_t check = BinaryReader_readInt32(reader);
- uint32_t nineSliceOffset = 0;
- if (check == -1) {
- spr->specialType = true;
- spr->sVersion = BinaryReader_readUint32(reader);
- spr->sSpriteType = BinaryReader_readUint32(reader);
- if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) {
- spr->gms2PlaybackSpeed = BinaryReader_readFloat32(reader);
- spr->gms2PlaybackSpeedType = BinaryReader_readUint32(reader);
- if (spr->sVersion >= 2) {
- BinaryReader_skip(reader, 4); //sequenceOffset;
- if (spr->sVersion >= 3) {
- nineSliceOffset = BinaryReader_readUint32(reader);
- }
- }
- check = BinaryReader_readUint32(reader);
- }
- }
-
- // 'check' is the texture count (start of SimpleList)
- spr->textureCount = (uint32_t)check;
- if (spr->textureCount > 0) {
- // Temporarily store the absolute file offsets here; parseTPAG resolves them in-place to TPAG indices once the TPAG table is known.
- spr->tpagIndices = safeMalloc(spr->textureCount * sizeof(int32_t));
- repeat(spr->textureCount, j) {
- spr->tpagIndices[j] = (int32_t) BinaryReader_readUint32(reader);
- }
- } else {
- spr->tpagIndices = nullptr;
- }
-
- // Collision mask data
- // sepMasks: 0 = axis-aligned rect (no mask data stored in some cases)
- // 1 = precise per-frame masks
- // 2 = rotated rect (no mask data)
- // Mask format: each bit = 1 pixel, MSB first, row-major
- // Width in bytes = (spriteWidth + 7) / 8, total = widthInBytes * spriteHeight
- // After all masks, data is padded to 4-byte alignment
- uint32_t maskDataCount = BinaryReader_readUint32(reader);
- spr->maskCount = maskDataCount;
- if (maskDataCount > 0 && spr->width > 0 && spr->height > 0) {
- uint32_t bytesPerRow = (spr->width + 7) / 8;
- uint32_t bytesPerMask = bytesPerRow * spr->height;
-
- if (spr->sepMasks == 1 || !skipLoadingPreciseMasksForNonPreciseSprites) {
- spr->masks = safeMalloc(maskDataCount * sizeof(uint8_t*));
- repeat(maskDataCount, j) {
- spr->masks[j] = safeMalloc(bytesPerMask);
- BinaryReader_readBytes(reader, spr->masks[j], bytesPerMask);
- }
- } else {
- BinaryReader_skip(reader, bytesPerMask * maskDataCount);
- spr->masks = nullptr;
- }
- // Pad the TOTAL mask data to 4-byte alignment (not per-mask)
- uint32_t totalMaskBytes = bytesPerMask * maskDataCount;
- uint32_t remainder = totalMaskBytes % 4;
- if (remainder != 0) {
- BinaryReader_skip(reader, 4 - remainder);
- }
- } else {
- spr->masks = nullptr;
- }
-
- // Nine-slice block (40 bytes). Located at nineSliceOffset (absolute file offset) elsewhere in the chunk.
- if (nineSliceOffset != 0) {
- size_t savedPos = BinaryReader_getPosition(reader);
- BinaryReader_seek(reader, (size_t) nineSliceOffset);
- spr->nsLeft = BinaryReader_readInt32(reader);
- spr->nsTop = BinaryReader_readInt32(reader);
- spr->nsRight = BinaryReader_readInt32(reader);
- spr->nsBottom = BinaryReader_readInt32(reader);
- spr->nineSliceEnabled = BinaryReader_readBool32(reader);
- repeat(5, j) {
- int32_t mode = BinaryReader_readInt32(reader);
- spr->nsTileModes[j] = (uint8_t) mode;
- }
- BinaryReader_seek(reader, savedPos);
- }
- }
-
- free(ptrs);
-}
-
-static void parseBGND(BinaryReader* reader, DataWin* dw) {
- Bgnd* b = &dw->bgnd;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- b->count = count;
-
- if (count == 0) { free(ptrs); b->backgrounds = nullptr; return; }
-
- b->backgrounds = safeCalloc(count, sizeof(Background));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- Background* bg = &b->backgrounds[i];
- bg->name = readStringPtr(reader, dw);
- bg->transparent = BinaryReader_readBool32(reader);
- bg->smooth = BinaryReader_readBool32(reader);
- bg->preload = BinaryReader_readBool32(reader);
- // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known.
- bg->tpagIndex = (int32_t) BinaryReader_readUint32(reader);
- if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) {
- bg->gms2UnknownAlways2 = BinaryReader_readUint32(reader);
- bg->gms2TileWidth = BinaryReader_readUint32(reader);
- bg->gms2TileHeight = BinaryReader_readUint32(reader);
- if (DataWin_isVersionAtLeast(dw, 2024, 14, 0, 1)) {
- bg->gms2TileSeparationX = BinaryReader_readUint32(reader);
- bg->gms2TileSeparationY = BinaryReader_readUint32(reader);
- }
- bg->gms2OutputBorderX = BinaryReader_readUint32(reader);
- bg->gms2OutputBorderY = BinaryReader_readUint32(reader);
- bg->gms2TileColumns = BinaryReader_readUint32(reader);
- bg->gms2ItemsPerTileCount = BinaryReader_readUint32(reader);
- bg->gms2TileCount = BinaryReader_readUint32(reader);
- bg->gms2ExportedSpriteIndex = BinaryReader_readInt32(reader);
- bg->gms2FrameLength = BinaryReader_readInt64(reader);
- int tileIdCount = bg->gms2TileCount * bg->gms2ItemsPerTileCount;
- bg->gms2TileIds = malloc(tileIdCount*sizeof(uint32_t));
- repeat(tileIdCount, j) {
- bg->gms2TileIds[j] = BinaryReader_readUint32(reader);
- }
- }
- }
- free(ptrs);
-}
-
-static void parsePATH(BinaryReader* reader, DataWin* dw) {
- PathChunk* p = &dw->path;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- p->count = count;
-
- if (count == 0) { free(ptrs); p->paths = nullptr; return; }
-
- p->paths = safeMalloc(count * sizeof(GamePath));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- GamePath* path = &p->paths[i];
- path->internalPoints = nullptr;
- path->internalPointCount = 0;
- path->length = 0.0;
- path->name = readStringPtr(reader, dw);
- path->isSmooth = BinaryReader_readBool32(reader);
- path->isClosed = BinaryReader_readBool32(reader);
- path->precision = BinaryReader_readUint32(reader);
-
- // Points SimpleList
- path->pointCount = BinaryReader_readUint32(reader);
- if (path->pointCount > 0) {
- path->points = safeMalloc(path->pointCount * sizeof(PathPoint));
- repeat(path->pointCount, j) {
- path->points[j].x = BinaryReader_readFloat32(reader);
- path->points[j].y = BinaryReader_readFloat32(reader);
- path->points[j].speed = BinaryReader_readFloat32(reader);
- }
- } else {
- path->points = nullptr;
- }
-
- // Precompute internal representation for path following
- GamePath_computeInternal(path);
- }
- free(ptrs);
-}
-
-static void parseSCPT(BinaryReader* reader, DataWin* dw) {
- Scpt* s = &dw->scpt;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- s->count = count;
-
- if (count == 0) { free(ptrs); s->scripts = nullptr; return; }
-
- s->scripts = safeMalloc(count * sizeof(Script));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- s->scripts[i].name = readStringPtr(reader, dw);
- s->scripts[i].codeId = BinaryReader_readInt32(reader);
- }
- free(ptrs);
-}
-
-static void parseGLOB(BinaryReader* reader, DataWin* dw) {
- Glob* g = &dw->glob;
-
- g->count = BinaryReader_readUint32(reader);
- if (g->count > 0) {
- g->codeIds = safeMalloc(g->count * sizeof(int32_t));
- repeat(g->count, i) {
- g->codeIds[i] = BinaryReader_readInt32(reader);
- }
- } else {
- g->codeIds = nullptr;
- }
-}
-
-static void parseSHDR(BinaryReader* reader, DataWin* dw) {
- Shdr* s = &dw->shdr;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- s->count = count;
-
- if (count == 0) { free(ptrs); s->shaders = nullptr; return; }
-
- s->shaders = safeMalloc(count * sizeof(Shader));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- Shader* sh = &s->shaders[i];
- sh->name = readStringPtr(reader, dw);
- sh->type = BinaryReader_readUint32(reader) & 0x7FFFFFFF;
- sh->glslES_Vertex = readStringPtr(reader, dw);
- sh->glslES_Fragment = readStringPtr(reader, dw);
- sh->glsl_Vertex = readStringPtr(reader, dw);
- sh->glsl_Fragment = readStringPtr(reader, dw);
- sh->hlsl9_Vertex = readStringPtr(reader, dw);
- sh->hlsl9_Fragment = readStringPtr(reader, dw);
- sh->hlsl11_VertexOffset = BinaryReader_readUint32(reader);
- sh->hlsl11_PixelOffset = BinaryReader_readUint32(reader);
-
- // Vertex attributes SimpleList
- sh->vertexAttributeCount = BinaryReader_readUint32(reader);
- if (sh->vertexAttributeCount > 0) {
- sh->vertexAttributes = safeMalloc(sh->vertexAttributeCount * sizeof(const char*));
- repeat(sh->vertexAttributeCount, j) {
- sh->vertexAttributes[j] = readStringPtr(reader, dw);
- }
- } else {
- sh->vertexAttributes = nullptr;
- }
-
- // Version field (bytecodeVersion > 13)
- sh->version = BinaryReader_readInt32(reader);
-
- sh->pssl_VertexOffset = BinaryReader_readUint32(reader);
- sh->pssl_VertexLen = BinaryReader_readUint32(reader);
- sh->pssl_PixelOffset = BinaryReader_readUint32(reader);
- sh->pssl_PixelLen = BinaryReader_readUint32(reader);
- sh->cgVita_VertexOffset = BinaryReader_readUint32(reader);
- sh->cgVita_VertexLen = BinaryReader_readUint32(reader);
- sh->cgVita_PixelOffset = BinaryReader_readUint32(reader);
- sh->cgVita_PixelLen = BinaryReader_readUint32(reader);
-
- if (sh->version >= 2) {
- sh->cgPS3_VertexOffset = BinaryReader_readUint32(reader);
- sh->cgPS3_VertexLen = BinaryReader_readUint32(reader);
- sh->cgPS3_PixelOffset = BinaryReader_readUint32(reader);
- sh->cgPS3_PixelLen = BinaryReader_readUint32(reader);
- } else {
- sh->cgPS3_VertexOffset = 0;
- sh->cgPS3_VertexLen = 0;
- sh->cgPS3_PixelOffset = 0;
- sh->cgPS3_PixelLen = 0;
- }
-
- // Blob data follows but we skip it (pointer list seeking handles position)
- }
- free(ptrs);
-}
-
-static void parseFONT(BinaryReader* reader, DataWin* dw) {
- FontChunk* f = &dw->font;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- f->count = count;
-
- if (count == 0) { free(ptrs); f->fonts = nullptr; return; }
-
- // We need to figure out how many uint32 fields are between here and the PointerList
- uint32_t fontOptionalCount = (dw->gen8.bytecodeVersion >= 17) ? 1u : 0u;
- {
- size_t baseAfterScaleY = (size_t) ptrs[0] + 40;
- for (uint32_t trial = fontOptionalCount; 4 >= trial; trial++) {
- size_t listStart = baseAfterScaleY + 4u * trial;
- BinaryReader_seek(reader, listStart);
- uint32_t probedGlyphCount = BinaryReader_readUint32(reader);
- if (probedGlyphCount == 0 || probedGlyphCount > 0x10000) continue;
- uint32_t probedFirstPtr = BinaryReader_readUint32(reader);
- size_t expectedFirstPtr = listStart + 4u + 4u * probedGlyphCount;
- if ((size_t) probedFirstPtr == expectedFirstPtr) {
- fontOptionalCount = trial;
- break;
- }
- }
- }
-
- f->fonts = safeMalloc(count * sizeof(Font));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- Font* font = &f->fonts[i];
- font->name = readStringPtr(reader, dw);
- font->displayName = readStringPtr(reader, dw);
- font->emSize = BinaryReader_readUint32(reader);
- font->bold = BinaryReader_readBool32(reader);
- font->italic = BinaryReader_readBool32(reader);
- font->rangeStart = BinaryReader_readUint16(reader);
- font->charset = BinaryReader_readUint8(reader);
- font->antiAliasing = BinaryReader_readUint8(reader);
- font->rangeEnd = BinaryReader_readUint32(reader);
- // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known.
- font->tpagIndex = (int32_t) BinaryReader_readUint32(reader);
- font->scaleX = BinaryReader_readFloat32(reader);
- font->scaleY = BinaryReader_readFloat32(reader);
- // Optional fields appear in this order when present: AscenderOffset (BC17+),
- // Ascender, SDFSpread, LineHeight. `fontOptionalCount` says how many are actually on disk.
- font->ascenderOffset = 0;
- font->ascender = 0;
- font->sdfSpread = 0;
- font->lineHeight = 0;
- font->hasAscender = false;
- font->hasSDFSpread = false;
- font->hasLineHeight = false;
- uint32_t readSoFar = 0;
- if (dw->gen8.bytecodeVersion >= 17 && fontOptionalCount > readSoFar) {
- font->ascenderOffset = BinaryReader_readInt32(reader);
- readSoFar++;
- }
- if (fontOptionalCount > readSoFar) {
- font->ascender = BinaryReader_readUint32(reader);
- font->hasAscender = true;
- readSoFar++;
- }
- if (fontOptionalCount > readSoFar) {
- font->sdfSpread = BinaryReader_readUint32(reader);
- font->hasSDFSpread = true;
- readSoFar++;
- }
- if (fontOptionalCount > readSoFar) {
- font->lineHeight = BinaryReader_readUint32(reader);
- font->hasLineHeight = true;
- readSoFar++;
- }
- font->isSpriteFont = false;
- font->spriteIndex = -1;
-
- // Glyphs PointerList
- uint32_t glyphCount;
- uint32_t* glyphPtrs = readPointerTable(reader, &glyphCount);
- font->glyphCount = glyphCount;
-
- uint32_t maxGlyphHeight = 0;
- if (glyphCount > 0) {
- font->glyphs = safeMalloc(glyphCount * sizeof(FontGlyph));
- repeat(glyphCount, j) {
- BinaryReader_seek(reader, glyphPtrs[j]);
- FontGlyph* glyph = &font->glyphs[j];
- glyph->character = BinaryReader_readUint16(reader);
- glyph->sourceX = BinaryReader_readUint16(reader);
- glyph->sourceY = BinaryReader_readUint16(reader);
- glyph->sourceWidth = BinaryReader_readUint16(reader);
- glyph->sourceHeight = BinaryReader_readUint16(reader);
- glyph->shift = BinaryReader_readInt16(reader);
- glyph->offset = BinaryReader_readInt16(reader);
-
- if (glyph->sourceHeight > maxGlyphHeight) maxGlyphHeight = glyph->sourceHeight;
-
- // Kerning SimpleListShort (uint16 count)
- glyph->kerningCount = BinaryReader_readUint16(reader);
- if (glyph->kerningCount > 0) {
- glyph->kerning = safeMalloc(glyph->kerningCount * sizeof(KerningPair));
- for (uint16_t k = 0; glyph->kerningCount > k; k++) {
- glyph->kerning[k].character = BinaryReader_readInt16(reader);
- glyph->kerning[k].shiftModifier = BinaryReader_readInt16(reader);
- }
- } else {
- glyph->kerning = nullptr;
- }
- }
- } else {
- font->glyphs = nullptr;
- }
- font->maxGlyphHeight = maxGlyphHeight;
- Font_buildGlyphLUT(font);
- free(glyphPtrs);
- }
- free(ptrs);
-
- // 512 bytes of trailing padding -- skipped by chunkEnd seek
-}
-
-static void parseTMLN(BinaryReader* reader, DataWin* dw) {
- Tmln* t = &dw->tmln;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- t->count = count;
-
- if (count == 0) { free(ptrs); t->timelines = nullptr; return; }
-
- t->timelines = safeMalloc(count * sizeof(Timeline));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- Timeline* tl = &t->timelines[i];
- tl->name = readStringPtr(reader, dw);
- tl->momentCount = BinaryReader_readUint32(reader);
-
- if (tl->momentCount > 0) {
- tl->moments = safeMalloc(tl->momentCount * sizeof(TimelineMoment));
-
- // Pass 1: Read step + event pointer pairs
- uint32_t* eventPtrs = safeMalloc(tl->momentCount * sizeof(uint32_t));
- repeat(tl->momentCount, j) {
- tl->moments[j].step = BinaryReader_readUint32(reader);
- eventPtrs[j] = BinaryReader_readUint32(reader);
- }
-
- // Pass 2: Parse event action lists
- repeat(tl->momentCount, j) {
- BinaryReader_seek(reader, eventPtrs[j]);
- tl->moments[j].actions = readEventActions(reader, dw, &tl->moments[j].actionCount);
- }
- free(eventPtrs);
- } else {
- tl->moments = nullptr;
- }
- }
- free(ptrs);
-}
-
-static void parseOBJT(BinaryReader* reader, DataWin* dw) {
- Objt* o = &dw->objt;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- o->count = count;
-
- if (count == 0) { free(ptrs); o->objects = nullptr; return; }
-
- // Detect GMS 2022.5+ by probing the first game object's event list structure.
- if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) {
- // Skip the 16 fixed uint32 header fields (name..angularDamping) to reach physicsVertexCount.
- BinaryReader_seek(reader, ptrs[0] + 16 * 4);
- int32_t vertexCount = BinaryReader_readInt32(reader);
- if (vertexCount >= 0) {
- // Skip friction + awake + kinematic (12 bytes) and physics vertices (8 bytes each).
- uint32_t skipCount = 12 + vertexCount * 8;
- uint32_t newLocation = reader->bufferPos + skipCount;
- bool isOldFormat = false;
- if (newLocation < reader->bufferSize) {
- BinaryReader_skip(reader, skipCount);
- uint32_t eventTypeCount = BinaryReader_readUint32(reader);
- if (eventTypeCount == OBJT_EVENT_TYPE_COUNT) {
- uint32_t firstSubEventPtr = BinaryReader_readUint32(reader);
- uint32_t currentAbsPos = (uint32_t) BinaryReader_getPosition(reader);
- // The remaining 14 outer-list pointers sit between here and the first sub-event list.
- if (firstSubEventPtr == currentAbsPos + 14 * 4) {
- isOldFormat = true;
- }
- }
- }
- if (!isOldFormat) {
- DataWin_bumpVersionTo(dw, 2022, 5, 0, 0);
- }
- }
- }
-
- o->objects = safeMalloc(count * sizeof(GameObject));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- GameObject* obj = &o->objects[i];
- obj->name = readStringPtr(reader, dw);
- obj->spriteId = BinaryReader_readInt32(reader);
- obj->visible = BinaryReader_readBool32(reader);
- if (DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) {
- obj->managed = BinaryReader_readBool32(reader);
- } else {
- obj->managed = false;
- }
- obj->solid = BinaryReader_readBool32(reader);
- obj->depth = BinaryReader_readInt32(reader);
- obj->persistent = BinaryReader_readBool32(reader);
- obj->parentId = BinaryReader_readInt32(reader);
- obj->textureMaskId = BinaryReader_readInt32(reader);
- obj->usesPhysics = BinaryReader_readBool32(reader);
- obj->isSensor = BinaryReader_readBool32(reader);
- obj->collisionShape = BinaryReader_readUint32(reader);
- obj->density = BinaryReader_readFloat32(reader);
- obj->restitution = BinaryReader_readFloat32(reader);
- obj->group = BinaryReader_readUint32(reader);
- obj->linearDamping = BinaryReader_readFloat32(reader);
- obj->angularDamping = BinaryReader_readFloat32(reader);
- obj->physicsVertexCount = BinaryReader_readInt32(reader);
- obj->friction = BinaryReader_readFloat32(reader);
- obj->awake = BinaryReader_readBool32(reader);
- obj->kinematic = BinaryReader_readBool32(reader);
-
- // Physics vertices
- if (obj->physicsVertexCount > 0) {
- obj->physicsVertices = safeMalloc(obj->physicsVertexCount * sizeof(PhysicsVertex));
- for (int32_t j = 0; obj->physicsVertexCount > j; j++) {
- obj->physicsVertices[j].x = BinaryReader_readFloat32(reader);
- obj->physicsVertices[j].y = BinaryReader_readFloat32(reader);
- }
- } else {
- obj->physicsVertices = nullptr;
- }
-
- // Events: UndertalePointerList>
- // Outer pointer list: one entry per event type
- // Inner pointer list: events for that type
- uint32_t eventTypeCount;
- uint32_t* eventTypePtrs = readPointerTable(reader, &eventTypeCount);
-
- for (uint32_t eventType = 0; eventTypeCount > eventType && OBJT_EVENT_TYPE_COUNT > eventType; eventType++) {
- BinaryReader_seek(reader, eventTypePtrs[eventType]);
-
- // Inner pointer list: events for this type
- uint32_t eventCount;
- uint32_t* eventPtrs = readPointerTable(reader, &eventCount);
-
- obj->eventLists[eventType].eventCount = eventCount;
-
- if (eventCount > 0) {
- obj->eventLists[eventType].events = safeMalloc(eventCount * sizeof(ObjectEvent));
- repeat(eventCount, j) {
- BinaryReader_seek(reader, eventPtrs[j]);
- obj->eventLists[eventType].events[j].eventSubtype = BinaryReader_readUint32(reader);
- obj->eventLists[eventType].events[j].actions = readEventActions(reader, dw, &obj->eventLists[eventType].events[j].actionCount);
- }
- } else {
- obj->eventLists[eventType].events = nullptr;
- }
-
- free(eventPtrs);
- }
-
- // Zero-fill any unused event type slots
- for (uint32_t eventType = eventTypeCount; OBJT_EVENT_TYPE_COUNT > eventType; eventType++) {
- obj->eventLists[eventType].eventCount = 0;
- obj->eventLists[eventType].events = nullptr;
- }
-
- free(eventTypePtrs);
- }
- free(ptrs);
-}
-
-// ===[ Room payload parsing helpers ]===
-// Each of these assumes the caller has seeked to the start of the relevant PointerList (where the uint32 "count" of the list is).
-// They allocate and populate the corresponding fields on Room.
-// They are used by both the eager parse path and the lazy load path (DataWin_loadRoomPayload).
-
-static void readRoomBackgrounds(BinaryReader* reader, Room* room) {
- uint32_t bgCount;
- uint32_t* bgPtrs = readPointerTable(reader, &bgCount);
- room->backgrounds = safeMalloc(8 * sizeof(RoomBackground));
- uint32_t fillEnd = bgCount < 8 ? bgCount : 8;
- for (uint32_t j = 0; fillEnd > j; j++) {
- BinaryReader_seek(reader, bgPtrs[j]);
- RoomBackground* bg = &room->backgrounds[j];
- bg->enabled = BinaryReader_readBool32(reader);
- bg->foreground = BinaryReader_readBool32(reader);
- bg->backgroundDefinition = BinaryReader_readInt32(reader);
- bg->x = BinaryReader_readInt32(reader);
- bg->y = BinaryReader_readInt32(reader);
- bg->tileX = BinaryReader_readInt32(reader);
- bg->tileY = BinaryReader_readInt32(reader);
- bg->speedX = BinaryReader_readInt32(reader);
- bg->speedY = BinaryReader_readInt32(reader);
- bg->stretch = BinaryReader_readBool32(reader);
- }
- for (uint32_t j = fillEnd; 8 > j; j++) {
- memset(&room->backgrounds[j], 0, sizeof(RoomBackground));
- }
- free(bgPtrs);
-}
-
-static void readRoomViews(BinaryReader* reader, Room* room) {
- uint32_t viewCount;
- uint32_t* viewPtrsArr = readPointerTable(reader, &viewCount);
- room->views = safeMalloc(8 * sizeof(RoomView));
- for (uint32_t j = 0; viewCount > j && 8 > j; j++) {
- BinaryReader_seek(reader, viewPtrsArr[j]);
- RoomView* view = &room->views[j];
- view->enabled = BinaryReader_readBool32(reader);
- view->viewX = BinaryReader_readInt32(reader);
- view->viewY = BinaryReader_readInt32(reader);
- view->viewWidth = BinaryReader_readInt32(reader);
- view->viewHeight = BinaryReader_readInt32(reader);
- view->portX = BinaryReader_readInt32(reader);
- view->portY = BinaryReader_readInt32(reader);
- view->portWidth = BinaryReader_readInt32(reader);
- view->portHeight = BinaryReader_readInt32(reader);
- view->borderX = BinaryReader_readUint32(reader);
- view->borderY = BinaryReader_readUint32(reader);
- view->speedX = BinaryReader_readInt32(reader);
- view->speedY = BinaryReader_readInt32(reader);
- view->objectId = BinaryReader_readInt32(reader);
- }
- for (uint32_t j = viewCount; 8 > j; j++) {
- memset(&room->views[j], 0, sizeof(RoomView));
- }
- free(viewPtrsArr);
-}
-
-static void readRoomGameObjects(BinaryReader* reader, DataWin* dw, Room* room) {
- uint32_t objCount;
- uint32_t* objPtrs = readPointerTable(reader, &objCount);
- room->gameObjectCount = objCount;
- if (objCount > 0) {
- room->gameObjects = safeMalloc(objCount * sizeof(RoomGameObject));
- repeat(objCount, j) {
- BinaryReader_seek(reader, objPtrs[j]);
- RoomGameObject* go = &room->gameObjects[j];
- go->x = BinaryReader_readInt32(reader);
- go->y = BinaryReader_readInt32(reader);
- go->objectDefinition = BinaryReader_readInt32(reader);
- go->instanceID = BinaryReader_readUint32(reader);
- go->creationCode = BinaryReader_readInt32(reader);
- go->scaleX = BinaryReader_readFloat32(reader);
- go->scaleY = BinaryReader_readFloat32(reader);
- if (DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) {
- go->imageSpeed = BinaryReader_readFloat32(reader);
- go->imageIndex = BinaryReader_readInt32(reader);
- } else {
- go->imageSpeed = 1.0f;
- go->imageIndex = 0;
- }
- go->color = BinaryReader_readUint32(reader);
- go->rotation = BinaryReader_readFloat32(reader);
- if (dw->gen8.bytecodeVersion >= 16) {
- go->preCreateCode = BinaryReader_readInt32(reader);
- } else {
- go->preCreateCode = -1;
- }
- }
- } else {
- room->gameObjects = nullptr;
- }
- free(objPtrs);
-}
-
-static void readRoomTiles(BinaryReader* reader, DataWin* dw, Room* room) {
- uint32_t tileCount;
- uint32_t* tilePtrs = readPointerTable(reader, &tileCount);
- room->tileCount = tileCount;
- if (tileCount > 0) {
- room->tiles = safeMalloc(tileCount * sizeof(RoomTile));
- repeat(tileCount, j) {
- BinaryReader_seek(reader, tilePtrs[j]);
- RoomTile* tile = &room->tiles[j];
- tile->x = BinaryReader_readInt32(reader);
- tile->y = BinaryReader_readInt32(reader);
- tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0);
- tile->backgroundDefinition = BinaryReader_readInt32(reader);
- tile->sourceX = BinaryReader_readInt32(reader);
- tile->sourceY = BinaryReader_readInt32(reader);
- tile->width = BinaryReader_readUint32(reader);
- tile->height = BinaryReader_readUint32(reader);
- tile->tileDepth = BinaryReader_readInt32(reader);
- tile->instanceID = BinaryReader_readUint32(reader);
- tile->scaleX = BinaryReader_readFloat32(reader);
- tile->scaleY = BinaryReader_readFloat32(reader);
- tile->color = BinaryReader_readUint32(reader);
- }
- } else {
- room->tiles = nullptr;
- }
- free(tilePtrs);
-}
-
-static void readRoomLayers(BinaryReader* reader, DataWin* dw, Room* room) {
- uint32_t layerCount;
- uint32_t* layerPtrs = readPointerTable(reader, &layerCount);
- room->layerCount = layerCount;
-
- if (layerCount == 0) {
- room->layers = nullptr;
- free(layerPtrs);
- return;
- }
-
- room->layers = safeMalloc(layerCount * sizeof(RoomLayer));
- repeat(layerCount, j) {
- BinaryReader_seek(reader, layerPtrs[j]);
- RoomLayer* layer = &room->layers[j];
- layer->name = readStringPtr(reader, dw);
- layer->id = BinaryReader_readUint32(reader);
- layer->type = BinaryReader_readUint32(reader);
- layer->depth = BinaryReader_readInt32(reader);
- layer->xOffset = BinaryReader_readFloat32(reader);
- layer->yOffset = BinaryReader_readFloat32(reader);
- layer->hSpeed = BinaryReader_readFloat32(reader);
- layer->vSpeed = BinaryReader_readFloat32(reader);
- layer->visible = BinaryReader_readBool32(reader);
- layer->assetsData = nullptr;
- layer->backgroundData = nullptr;
- layer->instancesData = nullptr;
- layer->tilesData = nullptr;
- if (DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) {
- // EffectEnabled (bool32), EffectType (string ptr), EffectProperties (SimpleList)
- BinaryReader_skip(reader, 4); // EffectEnabled
- BinaryReader_skip(reader, 4); // EffectType (string ptr)
- uint32_t effectPropCount = BinaryReader_readUint32(reader);
- // Each EffectProperty is 12 bytes: Kind(int32) + Name(ptr) + Value(ptr)
- BinaryReader_skip(reader, effectPropCount * 12);
- }
- switch (layer->type) {
- case RoomLayerType_Path:
- case RoomLayerType_Path2:
- break; // Nothing to do
- case RoomLayerType_Effect:
- // In GMS 2022.1+, Effect layer data is empty (fields moved to layer header).
- if (!DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) {
- BinaryReader_skip(reader, 4); // EffectType (string ptr)
- uint32_t propCount = BinaryReader_readUint32(reader);
- BinaryReader_skip(reader, propCount * 12);
- }
- break;
-
- case RoomLayerType_Assets: {
- RoomLayerAssetsData* assets = safeMalloc(sizeof(RoomLayerAssetsData));
- uint32_t legacyTilesPtr = BinaryReader_readUint32(reader);
- uint32_t spritesPtr = BinaryReader_readUint32(reader);
-
- BinaryReader_seek(reader, legacyTilesPtr);
- uint32_t *innerTilePtrs = readPointerTable(reader, &assets->legacyTileCount);
- if (assets->legacyTileCount > 0) {
- assets->legacyTiles = safeMalloc(assets->legacyTileCount * sizeof(RoomTile));
- repeat(assets->legacyTileCount, k) {
- BinaryReader_seek(reader, innerTilePtrs[k]);
- RoomTile* tile = &assets->legacyTiles[k];
- tile->x = BinaryReader_readInt32(reader);
- tile->y = BinaryReader_readInt32(reader);
- tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0);
- tile->backgroundDefinition = BinaryReader_readInt32(reader);
- tile->sourceX = BinaryReader_readInt32(reader);
- tile->sourceY = BinaryReader_readInt32(reader);
- tile->width = BinaryReader_readUint32(reader);
- tile->height = BinaryReader_readUint32(reader);
- tile->tileDepth = BinaryReader_readInt32(reader);
- tile->instanceID = BinaryReader_readUint32(reader);
- tile->scaleX = BinaryReader_readFloat32(reader);
- tile->scaleY = BinaryReader_readFloat32(reader);
- tile->color = BinaryReader_readUint32(reader);
- }
- } else {
- assets->legacyTiles = nullptr;
- }
- free(innerTilePtrs);
-
- BinaryReader_seek(reader, spritesPtr);
- uint32_t *spritePtrs = readPointerTable(reader, &assets->spriteCount);
- if (assets->spriteCount > 0) {
- assets->sprites = safeMalloc(assets->spriteCount * sizeof(SpriteInstance));
- repeat(assets->spriteCount, k) {
- BinaryReader_seek(reader, spritePtrs[k]);
- SpriteInstance* sprite = &assets->sprites[k];
- sprite->name = readStringPtr(reader, dw);
- sprite->spriteIndex = BinaryReader_readInt32(reader);
- sprite->x = BinaryReader_readInt32(reader);
- sprite->y = BinaryReader_readInt32(reader);
- sprite->scaleX = BinaryReader_readFloat32(reader);
- sprite->scaleY = BinaryReader_readFloat32(reader);
- sprite->color = BinaryReader_readUint32(reader);
- sprite->animationSpeed = BinaryReader_readFloat32(reader);
- sprite->animationSpeedType = BinaryReader_readUint32(reader);
- sprite->frameIndex = BinaryReader_readFloat32(reader);
- sprite->rotation = BinaryReader_readFloat32(reader);
- }
- } else {
- assets->sprites = nullptr;
- }
- free(spritePtrs);
-
- layer->assetsData = assets;
- break;
- }
-
- case RoomLayerType_Background: {
- RoomLayerBackgroundData* bg = safeMalloc(sizeof(RoomLayerBackgroundData));
- bg->visible = BinaryReader_readBool32(reader);
- bg->foreground = BinaryReader_readBool32(reader);
- bg->spriteIndex = BinaryReader_readInt32(reader);
- bg->hTiled = BinaryReader_readBool32(reader);
- bg->vTiled = BinaryReader_readBool32(reader);
- bg->stretch = BinaryReader_readBool32(reader);
- bg->color = BinaryReader_readUint32(reader);
- bg->firstFrame = BinaryReader_readFloat32(reader);
- bg->animSpeed = BinaryReader_readFloat32(reader);
- bg->animSpeedType = BinaryReader_readUint32(reader);
- layer->backgroundData = bg;
- break;
- }
- case RoomLayerType_Instances: {
- RoomLayerInstancesData* inst = safeMalloc(sizeof(RoomLayerInstancesData));
- inst->instanceCount = BinaryReader_readUint32(reader);
- if (inst->instanceCount > 0) {
- inst->instanceIds = safeMalloc(inst->instanceCount * sizeof(uint32_t));
- repeat(inst->instanceCount, k) {
- inst->instanceIds[k] = BinaryReader_readUint32(reader);
- }
- } else {
- inst->instanceIds = nullptr;
- }
- layer->instancesData = inst;
- break;
- }
- case RoomLayerType_Tiles: {
- RoomLayerTilesData* tiles = safeMalloc(sizeof(RoomLayerTilesData));
- tiles->backgroundIndex = BinaryReader_readInt32(reader);
- tiles->tilesX = BinaryReader_readUint32(reader);
- tiles->tilesY = BinaryReader_readUint32(reader);
- uint32_t totalTiles = tiles->tilesX * tiles->tilesY;
- if (totalTiles > 0) {
- tiles->tileData = safeMalloc(totalTiles * sizeof(uint32_t));
- repeat(totalTiles, k) {
- tiles->tileData[k] = BinaryReader_readUint32(reader);
- }
- } else {
- tiles->tileData = nullptr;
- }
- layer->tilesData = tiles;
- break;
- }
- default: {
- fprintf(stderr, "Unsupported Room Layer Type %u\n", layer->type);
- exit(0);
- }
- }
- }
- free(layerPtrs);
-}
-
-// Reads all 5 payload sections for a single room via the given reader.
-// Assumes the caller has populated room->*FileOffset from the header pass.
-static void readRoomPayload(BinaryReader* reader, DataWin* dw, Room* room) {
- require(!room->payloadLoaded);
-
- BinaryReader_seek(reader, room->backgroundsFileOffset);
- readRoomBackgrounds(reader, room);
-
- BinaryReader_seek(reader, room->viewsFileOffset);
- readRoomViews(reader, room);
-
- BinaryReader_seek(reader, room->gameObjectsFileOffset);
- readRoomGameObjects(reader, dw, room);
-
- BinaryReader_seek(reader, room->tilesFileOffset);
- readRoomTiles(reader, dw, room);
-
- room->layerCount = 0;
- room->layers = nullptr;
- if (room->layersFileOffset != 0) {
- BinaryReader_seek(reader, room->layersFileOffset);
- readRoomLayers(reader, dw, room);
- }
-
- room->payloadLoaded = true;
-}
-
-// Returns true when "name" is in the eager-load set.
-static bool isRoomNameInEagerList(const char* name, StringBooleanEntry* eagerSet) {
- if (name == nullptr || eagerSet == nullptr) return false;
- return shgeti(eagerSet, name) >= 0;
-}
-
-static void parseROOM(BinaryReader* reader, DataWin* dw, bool lazyLoadRooms, StringBooleanEntry* eagerlyLoadedRooms) {
- RoomChunk* rc = &dw->room;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- rc->count = count;
-
- if (count == 0) { free(ptrs); rc->rooms = nullptr; return; }
-
- // Detect whether RoomGameObject includes ImageSpeed/ImageIndex fields (added in GMS 2.2.2.302).
- // UndertaleModTool detects this via the distance between the first two game object pointers: 40 bytes = legacy format, 48 bytes = new format with ImageSpeed+ImageIndex.
- // We skip if we already know that we are at or above 2.2.2.302.
- if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0) && !DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) {
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- // Room header layout (before gameObjectsPtr): name, caption, width, height, speed, persistent,
- // bgColor, drawBgColor, creationCodeId, flags, backgroundsPtr, viewsPtr = 12 uint32s.
- BinaryReader_skip(reader, 12 * 4);
- uint32_t gameObjectsPtr = BinaryReader_readUint32(reader);
- BinaryReader_seek(reader, gameObjectsPtr);
- uint32_t objCount = BinaryReader_readUint32(reader);
- if (objCount >= 2) {
- uint32_t firstPtr = BinaryReader_readUint32(reader);
- uint32_t secondPtr = BinaryReader_readUint32(reader);
- if (secondPtr - firstPtr == 48) {
- DataWin_bumpVersionTo(dw, 2, 2, 2, 302);
- }
- break;
- }
- }
- }
-
- // Detect whether Layer headers include EffectEnabled/EffectType/EffectProperties fields (added in GMS 2022.1).
- if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) {
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- // Room header before layersPtr: 22 uint32s (name..metersPerPixel).
- BinaryReader_skip(reader, 22 * 4);
- uint32_t layersPtr = BinaryReader_readUint32(reader);
- uint32_t seqnPtr = BinaryReader_readUint32(reader);
- BinaryReader_seek(reader, layersPtr);
- uint32_t layerCount = BinaryReader_readUint32(reader);
- if (layerCount == 0) continue;
- uint32_t jumpOffset = BinaryReader_readUint32(reader);
- uint32_t nextOffset = (layerCount == 1) ? seqnPtr : BinaryReader_readUint32(reader);
- // Layer header: name(4) id(4) type(4) depth(4) xOff(4) yOff(4) hSpd(4) vSpd(4) visible(4) = 9 uint32s = 36 bytes.
- // jumpOffset points to start of the layer; we seek to jumpOffset+8 to skip name+id then read type.
- BinaryReader_seek(reader, jumpOffset + 8);
- uint32_t layerType = BinaryReader_readUint32(reader);
- if (layerType == RoomLayerType_Path || layerType == RoomLayerType_Path2) continue;
- bool detected = false;
- switch (layerType) {
- case RoomLayerType_Background: {
- // After type, there's depth+xOff+yOff+hSpd+vSpd+visible = 6*4 = 24, then 10 background fields = 40 bytes.
- // Total legacy body after type read: 24 + 40 = 64 bytes. 2022.1 adds effect data > 64 bytes of additional data past the next layer boundary.
- size_t absPos = BinaryReader_getPosition(reader);
- if (nextOffset - absPos > 16 * 4) detected = true;
- break;
- }
- case RoomLayerType_Instances: {
- BinaryReader_skip(reader, 6 * 4);
- uint32_t instanceCount = BinaryReader_readUint32(reader);
- size_t absPos = BinaryReader_getPosition(reader);
- if (nextOffset - absPos != instanceCount * 4) detected = true;
- break;
- }
- case RoomLayerType_Assets: {
- BinaryReader_skip(reader, 6 * 4);
- uint32_t tileOffset = BinaryReader_readUint32(reader);
- size_t absPos = BinaryReader_getPosition(reader);
- if (tileOffset != absPos + 8 && tileOffset != absPos + 12) detected = true;
- break;
- }
- case RoomLayerType_Tiles: {
- BinaryReader_skip(reader, 7 * 4);
- uint32_t tileMapWidth = BinaryReader_readUint32(reader);
- uint32_t tileMapHeight = BinaryReader_readUint32(reader);
- size_t absPos = BinaryReader_getPosition(reader);
- if (nextOffset - absPos != tileMapWidth * tileMapHeight * 4) detected = true;
- break;
- }
- case RoomLayerType_Effect: {
- BinaryReader_skip(reader, 7 * 4);
- uint32_t propertyCount = BinaryReader_readUint32(reader);
- size_t absPos = BinaryReader_getPosition(reader);
- if (nextOffset - absPos != propertyCount * 3 * 4) detected = true;
- break;
- }
- }
- if (detected) DataWin_bumpVersionTo(dw, 2022, 1, 0, 0);
- break;
- }
- }
-
- rc->rooms = safeCalloc(count, sizeof(Room));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- Room* room = &rc->rooms[i];
-
- // ===[ Header pass ]===
- room->name = readStringPtr(reader, dw);
- room->caption = readStringPtr(reader, dw);
- room->width = BinaryReader_readUint32(reader);
- room->height = BinaryReader_readUint32(reader);
- room->speed = BinaryReader_readUint32(reader);
- room->persistent = BinaryReader_readBool32(reader);
- room->backgroundColor = BinaryReader_readUint32(reader);
- room->drawBackgroundColor = BinaryReader_readBool32(reader);
- room->creationCodeId = BinaryReader_readInt32(reader);
- room->flags = BinaryReader_readUint32(reader);
- room->backgroundsFileOffset = BinaryReader_readUint32(reader);
- room->viewsFileOffset = BinaryReader_readUint32(reader);
- room->gameObjectsFileOffset = BinaryReader_readUint32(reader);
- room->tilesFileOffset = BinaryReader_readUint32(reader);
- room->world = BinaryReader_readBool32(reader);
- room->top = BinaryReader_readUint32(reader);
- room->left = BinaryReader_readUint32(reader);
- room->right = BinaryReader_readUint32(reader);
- room->bottom = BinaryReader_readUint32(reader);
- room->gravityX = BinaryReader_readFloat32(reader);
- room->gravityY = BinaryReader_readFloat32(reader);
- room->metersPerPixel = BinaryReader_readFloat32(reader);
- if (DataWin_isVersionAtLeast(dw, 2024, 13, 0, 0)) {
- // skip instanceCreationOrderIDs
- int icCount = BinaryReader_readInt32(reader);
- BinaryReader_skip(reader, sizeof(int32_t) * icCount);
- }
- room->layersFileOffset = 0;
- if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) {
- room->layersFileOffset = BinaryReader_readUint32(reader);
- if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0)) {
- BinaryReader_skip(reader, 4); // sequencesPtr
- }
- }
-
- room->payloadLoaded = false;
- room->eagerlyLoaded = false;
- room->backgrounds = nullptr;
- room->views = nullptr;
- room->gameObjects = nullptr;
- room->gameObjectCount = 0;
- room->tiles = nullptr;
- room->tileCount = 0;
- room->layers = nullptr;
- room->layerCount = 0;
-
- // Load the room payload if needed
- bool eager = !lazyLoadRooms || isRoomNameInEagerList(room->name, eagerlyLoadedRooms);
- if (eager) {
- readRoomPayload(reader, dw, room);
- if (lazyLoadRooms) {
- room->eagerlyLoaded = true;
- }
- }
- }
- free(ptrs);
-}
-
-// Sprite/Background/Font initially store an absolute file offset to their TexturePageItem (since SPRT/BGND/FONT are parsed before TPAG).
-// resolveAllTPAGReferences translates those offsets to TPAG indices once the table is known. ptrs[] is the TPAG pointer table in monotonically increasing file order, so we can binary search it.
-// Offsets that don't resolve (or are 0) become -1.
-static int32_t findTPAGIndexByOffset(uint32_t* ptrs, uint32_t count, uint32_t offset) {
- if (offset == 0) return -1;
- uint32_t lo = 0, hi = count;
- while (hi > lo) {
- uint32_t mid = (lo + hi) >> 1;
- uint32_t v = ptrs[mid];
- if (v == offset) return (int32_t) mid;
- if (offset > v) lo = mid + 1; else hi = mid;
- }
- return -1;
-}
-
-static void resolveAllTPAGReferences(DataWin* dw, uint32_t* ptrs, uint32_t count) {
- repeat(dw->sprt.count, i) {
- Sprite* spr = &dw->sprt.sprites[i];
- repeat(spr->textureCount, j) {
- spr->tpagIndices[j] = findTPAGIndexByOffset(ptrs, count, (uint32_t) spr->tpagIndices[j]);
- }
- }
- repeat(dw->bgnd.count, i) {
- Background* bg = &dw->bgnd.backgrounds[i];
- bg->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) bg->tpagIndex);
- }
- repeat(dw->font.count, i) {
- Font* fnt = &dw->font.fonts[i];
- fnt->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) fnt->tpagIndex);
- }
-}
-
-static void parseTPAG(BinaryReader* reader, DataWin* dw) {
- Tpag* t = &dw->tpag;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- t->count = count;
-
- if (count == 0) { free(ptrs); t->items = nullptr; return; }
-
- t->items = safeMalloc(count * sizeof(TexturePageItem));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- TexturePageItem* item = &t->items[i];
- item->sourceX = BinaryReader_readUint16(reader);
- item->sourceY = BinaryReader_readUint16(reader);
- item->sourceWidth = BinaryReader_readUint16(reader);
- item->sourceHeight = BinaryReader_readUint16(reader);
- item->targetX = BinaryReader_readUint16(reader);
- item->targetY = BinaryReader_readUint16(reader);
- item->targetWidth = BinaryReader_readUint16(reader);
- item->targetHeight = BinaryReader_readUint16(reader);
- item->boundingWidth = BinaryReader_readUint16(reader);
- item->boundingHeight = BinaryReader_readUint16(reader);
- item->texturePageId = BinaryReader_readInt16(reader);
- }
-
- resolveAllTPAGReferences(dw, ptrs, count);
-
- free(ptrs);
-}
-
-static void parseCODE(BinaryReader* reader, DataWin* dw, uint32_t chunkLength, size_t chunkDataStart) {
- Code* c = &dw->code;
-
- if (chunkLength == 0) {
- // YYC-compiled game, no bytecode
- c->count = 0;
- c->entries = nullptr;
- return;
- }
-
- // Standard pointer list at chunk start. Each entry has a relative offset
- // (bytecodeRelAddr) that points to the actual bytecode blob elsewhere in the chunk.
-
- uint32_t codeCount;
- uint32_t* codePtrs = readPointerTable(reader, &codeCount);
- c->count = codeCount;
-
- if (codeCount == 0) { free(codePtrs); c->entries = nullptr; return; }
-
- c->entries = safeMalloc(codeCount * sizeof(CodeEntry));
- repeat(codeCount, i) {
- BinaryReader_seek(reader, codePtrs[i]);
- CodeEntry* entry = &c->entries[i];
- entry->name = readStringPtr(reader, dw);
- entry->length = BinaryReader_readUint32(reader);
- entry->localsCount = BinaryReader_readUint16(reader);
- entry->argumentsCount = BinaryReader_readUint16(reader);
-
- // bytecodeRelAddr is relative to the position of this field
- size_t relAddrFieldPos = BinaryReader_getPosition(reader);
- int32_t bytecodeRelAddr = BinaryReader_readInt32(reader);
- entry->bytecodeAbsoluteOffset = (uint32_t)((int64_t)relAddrFieldPos + bytecodeRelAddr);
-
- entry->offset = BinaryReader_readUint32(reader);
- }
- free(codePtrs);
-
- // Compute bytecode blob range and load into owned buffer.
- // The bytecode blob starts at the minimum bytecodeAbsoluteOffset and
- // extends to the end of the CODE chunk.
- uint32_t blobStart = c->entries[0].bytecodeAbsoluteOffset;
- repeat(codeCount, i) {
- if (c->entries[i].bytecodeAbsoluteOffset < blobStart) {
- blobStart = c->entries[i].bytecodeAbsoluteOffset;
- }
- }
- size_t chunkEnd = chunkDataStart + chunkLength;
- size_t blobSize = chunkEnd - blobStart;
-
- dw->bytecodeBufferBase = blobStart;
- dw->bytecodeBuffer = BinaryReader_readBytesAt(reader, blobStart, blobSize);
-}
-
-static void parseVARI(BinaryReader* reader, DataWin* dw, uint32_t chunkLength) {
- Vari* v = &dw->vari;
-
- v->varCount1 = BinaryReader_readUint32(reader);
- v->varCount2 = BinaryReader_readUint32(reader);
- v->maxLocalVarCount = BinaryReader_readUint32(reader);
-
- // Variable entries are packed sequentially (no pointer table)
- // Number of entries = (chunkLength - 12) / 20
- v->variableCount = (chunkLength - 12) / 20;
-
- if (v->variableCount > 0) {
- v->variables = safeMalloc(v->variableCount * sizeof(Variable));
- repeat(v->variableCount, i) {
- Variable* var = &v->variables[i];
- var->name = readStringPtr(reader, dw);
- var->instanceType = BinaryReader_readInt32(reader);
- var->varID = BinaryReader_readInt32(reader);
- var->occurrences = BinaryReader_readUint32(reader);
- var->firstAddress = BinaryReader_readUint32(reader);
- }
- } else {
- v->variables = nullptr;
- }
-}
-
-static void parseFUNC(BinaryReader* reader, DataWin* dw) {
- Func* f = &dw->func;
-
- // Part 1: Functions SimpleList
- f->functionCount = BinaryReader_readUint32(reader);
- if (f->functionCount > 0) {
- f->functions = safeMalloc(f->functionCount * sizeof(Function));
- repeat(f->functionCount, i) {
- f->functions[i].name = readStringPtr(reader, dw);
- f->functions[i].occurrences = BinaryReader_readUint32(reader);
- uint32_t rawAddr = BinaryReader_readUint32(reader);
- // In GMS 2.3+, firstAddress points to the operand word (instruction + 4), not the instruction itself
- if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && rawAddr != (uint32_t) -1) {
- rawAddr -= 4;
- }
- f->functions[i].firstAddress = rawAddr;
- }
- } else {
- f->functions = nullptr;
- }
-
- // Part 2: Code Locals SimpleList
- f->codeLocalsCount = BinaryReader_readUint32(reader);
- if (f->codeLocalsCount > 0) {
- f->codeLocals = safeMalloc(f->codeLocalsCount * sizeof(CodeLocals));
- repeat(f->codeLocalsCount, i) {
- CodeLocals* cl = &f->codeLocals[i];
- cl->localVarCount = BinaryReader_readUint32(reader);
- cl->name = readStringPtr(reader, dw);
-
- if (cl->localVarCount > 0) {
- cl->locals = safeMalloc(cl->localVarCount * sizeof(LocalVar));
- repeat(cl->localVarCount, j) {
- cl->locals[j].varID = BinaryReader_readUint32(reader);
- cl->locals[j].name = readStringPtr(reader, dw);
- }
- } else {
- cl->locals = nullptr;
- }
- }
- } else {
- f->codeLocals = nullptr;
- }
-}
-
-static void parseSTRG(BinaryReader* reader, DataWin* dw) {
- Strg* s = &dw->strg;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- s->count = count;
-
- if (count == 0) { free(ptrs); s->strings = nullptr; return; }
-
- s->strings = safeMalloc(count * sizeof(const char*));
- repeat(count, i) {
- // Pointer table points to the string's length prefix.
- // The actual string content starts 4 bytes after.
- s->strings[i] = (const char*)(dw->strgBuffer + (ptrs[i] + 4 - dw->strgBufferBase));
- }
- free(ptrs);
-}
-
-static void parseTXTR(BinaryReader* reader, DataWin* dw, size_t chunkEnd) {
- Txtr* t = &dw->txtr;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- t->count = count;
-
- if (count == 0) { free(ptrs); t->textures = nullptr; return; }
-
- // Read metadata entries
- bool hasGeneratedMips = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0);
-
- // Detect GMS 2022.3+ (TextureBlockSize field) and 2022.9+ (Width/Height/IndexInGroup fields) by probing the distance between the first two entry pointers.
- // Only works when there are at least 2 textures (which is almost always the case for real games).
- // Layouts:
- // pre-2022.3: scaled+generatedMips+blobOffset = 12 bytes
- // 2022.3+: ... + textureBlockSize = 16 bytes
- // 2022.9+: ... + width + height + indexInGroup = 28 bytes
- bool has2022_3 = DataWin_isVersionAtLeast(dw, 2022, 3, 0, 0);
- bool has2022_9 = DataWin_isVersionAtLeast(dw, 2022, 9, 0, 0);
- if (count >= 2 && hasGeneratedMips && !has2022_9) {
- uint32_t diff = ptrs[1] - ptrs[0];
- if (diff == 28) {
- DataWin_bumpVersionTo(dw, 2022, 9, 0, 0);
- has2022_3 = true;
- has2022_9 = true;
- } else if (diff == 16 && !has2022_3) {
- DataWin_bumpVersionTo(dw, 2022, 3, 0, 0);
- has2022_3 = true;
- }
- }
-
- t->textures = safeMalloc(count * sizeof(Texture));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- t->textures[i].scaled = BinaryReader_readUint32(reader);
- if (hasGeneratedMips) {
- t->textures[i].generatedMips = BinaryReader_readUint32(reader);
- } else {
- t->textures[i].generatedMips = 0;
- }
- if (has2022_3) {
- t->textures[i].textureBlockSize = BinaryReader_readUint32(reader);
- } else {
- t->textures[i].textureBlockSize = 0;
- }
- if (has2022_9) {
- t->textures[i].textureWidth = BinaryReader_readInt32(reader);
- t->textures[i].textureHeight = BinaryReader_readInt32(reader);
- t->textures[i].indexInGroup = BinaryReader_readInt32(reader);
- } else {
- t->textures[i].textureWidth = 0;
- t->textures[i].textureHeight = 0;
- t->textures[i].indexInGroup = 0;
- }
- t->textures[i].blobOffset = BinaryReader_readUint32(reader);
- t->textures[i].blobData = nullptr;
- }
- free(ptrs);
-
- // Compute blob sizes from successive offsets
- repeat(count, i) {
- if (t->textures[i].blobOffset == 0) {
- t->textures[i].blobSize = 0; // external texture
- continue;
- }
- if (count > i + 1 && t->textures[i + 1].blobOffset != 0) {
- t->textures[i].blobSize = t->textures[i + 1].blobOffset - t->textures[i].blobOffset;
- } else {
- t->textures[i].blobSize = (uint32_t)(chunkEnd - t->textures[i].blobOffset);
- }
- }
-
- // Load blob data into owned buffers
- repeat(count, i) {
- if (t->textures[i].blobOffset == 0 || t->textures[i].blobSize == 0) continue;
- t->textures[i].blobData = BinaryReader_readBytesAt(reader, t->textures[i].blobOffset, t->textures[i].blobSize);
- }
-}
-
-static void parseAUDO(BinaryReader* reader, DataWin* dw) {
- Audo* a = &dw->audo;
-
- uint32_t count;
- uint32_t* ptrs = readPointerTable(reader, &count);
- a->count = count;
-
- if (count == 0) { free(ptrs); a->entries = nullptr; return; }
-
- a->entries = safeMalloc(count * sizeof(AudioEntry));
- repeat(count, i) {
- BinaryReader_seek(reader, ptrs[i]);
- a->entries[i].dataSize = BinaryReader_readUint32(reader);
- a->entries[i].dataOffset = (uint32_t)BinaryReader_getPosition(reader);
- // Load audio data into owned buffer
- if (a->entries[i].dataSize > 0) {
- a->entries[i].data = safeMalloc(a->entries[i].dataSize);
- BinaryReader_readBytes(reader, a->entries[i].data, a->entries[i].dataSize);
- } else {
- a->entries[i].data = nullptr;
- }
- }
- free(ptrs);
-}
-
-// ===[ MAIN PARSE FUNCTION ]===
-
-DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) {
- FILE* file = fopen(filePath, "rb");
- if (!file) {
- fprintf(stderr, "Failed to open file: %s\n", filePath);
- exit(1);
- }
-
- // Use a large read buffer to reduce the number of physical reads
- // This is critical for slow I/O devices like the PS2 CDVD drive, where each fread
- // call would otherwise trigger a separate disc read of just a few sectors
- setvbuf(file, nullptr, _IOFBF, 128 * 1024);
-
- fseek(file, 0, SEEK_END);
- long fileSize = ftell(file);
- fseek(file, 0, SEEK_SET);
-
- if (fileSize <= 0) {
- fprintf(stderr, "Invalid file size: %ld\n", fileSize);
- fclose(file);
- exit(1);
- }
-
- // Allocate and zero-initialize DataWin
- DataWin* dw = safeCalloc(1, sizeof(DataWin));
-
- BinaryReader reader = BinaryReader_create(file, (size_t) fileSize);
-
- // Validate FORM header
- char formMagic[4];
- BinaryReader_readBytes(&reader, formMagic, 4);
- if (memcmp(formMagic, "FORM", 4) != 0) {
- fprintf(stderr, "Invalid file: expected FORM magic, got '%.4s'\n", formMagic);
- free(dw);
- fclose(file);
- exit(1);
- }
-
- uint32_t formLength = BinaryReader_readUint32(&reader);
- (void) formLength;
-
- // Pass 1: Count total chunks and find STRG chunk offset.
- // All other chunks reference strings from STRG, so it must be loaded first.
- // We also check if the CODE chunk exists.
- int totalChunks = 0;
- bool codeExists = false;
- BinaryReader_seek(&reader, 8); // reset to after FORM header
-
- while ((size_t) fileSize > BinaryReader_getPosition(&reader)) {
- if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break;
-
- char chunkName[5] = {0};
- BinaryReader_readBytes(&reader, chunkName, 4);
- uint32_t chunkLength = BinaryReader_readUint32(&reader);
- size_t chunkDataStart = BinaryReader_getPosition(&reader);
-
- if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) {
- dw->strgBufferBase = chunkDataStart;
- dw->strgBuffer = BinaryReader_readBytesAt(&reader, chunkDataStart, chunkLength);
- }
-
- if ((memcmp(chunkName, "CODE", 4) == 0) && chunkLength > 0) {
- codeExists = true;
- }
-
- // Bump detected version based on chunk presence, so later chunks can use the right version during parsing (parseOBJT needs to know we're >= 2.3 to probe for the GMS 2022.5+ Managed field).
- if (memcmp(chunkName, "ACRV", 4) == 0 || memcmp(chunkName, "SEQN", 4) == 0 || memcmp(chunkName, "TAGS", 4) == 0) {
- DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
- } else if (memcmp(chunkName, "FEDS", 4) == 0) {
- DataWin_bumpVersionTo(dw, 2, 3, 6, 0);
- } else if (memcmp(chunkName, "FEAT", 4) == 0) {
- DataWin_bumpVersionTo(dw, 2022, 8, 0, 0);
- } else if (memcmp(chunkName, "UILR", 4) == 0) {
- DataWin_bumpVersionTo(dw, 2024, 13, 0, 0);
- } else if (memcmp(chunkName, "PSEM", 4) == 0 || memcmp(chunkName, "PSYS", 4) == 0) {
- DataWin_bumpVersionTo(dw, 2023, 2, 0, 0);
- }
-
- BinaryReader_seek(&reader, chunkDataStart + chunkLength);
- totalChunks++;
- }
-
- if (!codeExists && options.parseCode) {
- fprintf(stderr, "CODE chunk does not exist or is empty! This usually means you're loading a YYC game.\n");
- fclose(file);
- exit(1);
- }
-
- // Pass 2: Parse all chunks
- // For each chunk that will be parsed, we bulk-read the entire chunk into memory first
- // and then parse from the memory buffer. This dramatically reduces the number of physical
- // reads on slow I/O devices like the PS2 CDVD drive.
- BinaryReader_seek(&reader, 8); // skip past FORM header
- int chunkIndex = 0;
- while ((size_t) fileSize > BinaryReader_getPosition(&reader)) {
- if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break;
-
- char chunkName[5] = {0};
- BinaryReader_readBytes(&reader, chunkName, 4);
- uint32_t chunkLength = BinaryReader_readUint32(&reader);
- size_t chunkDataStart = BinaryReader_getPosition(&reader);
- size_t chunkEnd = chunkDataStart + chunkLength;
-
- if (options.progressCallback) {
- options.progressCallback(chunkName, chunkIndex, totalChunks, dw, options.progressCallbackUserData);
- }
-
- // Determine if this chunk will be parsed (and thus needs bulk loading)
- bool shouldParse =
- (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) ||
- (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) ||
- (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) ||
- (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) ||
- (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) ||
- (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) ||
- (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) ||
- (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) ||
- (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) ||
- (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) ||
- (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) ||
- (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) ||
- (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) ||
- (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) ||
- (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) ||
- (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) ||
- (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) ||
- (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) ||
- (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) ||
- (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) ||
- (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) ||
- (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) ||
- (options.parseAudo && memcmp(chunkName, "AUDO", 4) == 0);
-
- // Bulk-read the chunk data into memory for fast parsing
- uint8_t* chunkBuffer = nullptr;
- if (shouldParse && chunkLength > 0) {
- chunkBuffer = safeMalloc(chunkLength);
- size_t read = fread(chunkBuffer, 1, chunkLength, reader.file);
- if (read != chunkLength) {
- fprintf(stderr, "DataWin: short read on chunk %.4s (expected %u, got %zu)\n", chunkName, chunkLength, read);
- exit(1);
- }
- BinaryReader_setBuffer(&reader, chunkBuffer, chunkDataStart, chunkLength);
- }
-
- if (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) {
- parseGEN8(&reader, dw);
- } else if (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) {
- parseOPTN(&reader, dw);
- } else if (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) {
- parseLANG(&reader, dw);
- } else if (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) {
- parseEXTN(&reader, dw);
- } else if (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) {
- parseSOND(&reader, dw);
- } else if (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) {
- parseAGRP(&reader, dw);
- } else if (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) {
- parseSPRT(&reader, dw, options.skipLoadingPreciseMasksForNonPreciseSprites);
- } else if (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) {
- parseBGND(&reader, dw);
- } else if (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) {
- parsePATH(&reader, dw);
- } else if (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) {
- parseSCPT(&reader, dw);
- } else if (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) {
- parseGLOB(&reader, dw);
- } else if (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) {
- parseSHDR(&reader, dw);
- } else if (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) {
- parseFONT(&reader, dw);
- } else if (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) {
- parseTMLN(&reader, dw);
- } else if (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) {
- parseOBJT(&reader, dw);
- } else if (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) {
- parseROOM(&reader, dw, options.lazyLoadRooms, options.eagerlyLoadedRooms);
- } else if (memcmp(chunkName, "DAFL", 4) == 0) {
- // Empty chunk, nothing to parse
- } else if (memcmp(chunkName, "EMBI", 4) == 0) {
- // Embedded Images chunk
- } else if (memcmp(chunkName, "TGIN", 4) == 0) {
- // Texture Group Info chunk (bytecodeVersion >= 17)
- } else if (memcmp(chunkName, "ACRV", 4) == 0) {
- // Animation Curves chunk (GMS 2.3+)
- DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
- } else if (memcmp(chunkName, "SEQN", 4) == 0) {
- // Sequences chunk (GMS 2.3+)
- DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
- } else if (memcmp(chunkName, "TAGS", 4) == 0) {
- // Tags chunk (GMS 2.3+)
- DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
- } else if (memcmp(chunkName, "FEDS", 4) == 0) {
- // Filter Effects Data chunk (GMS 2.3.6+)
- DataWin_bumpVersionTo(dw, 2, 3, 6, 0);
- } else if (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) {
- parseTPAG(&reader, dw);
- } else if (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) {
- parseCODE(&reader, dw, chunkLength, chunkDataStart);
- } else if (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) {
- parseVARI(&reader, dw, chunkLength);
- } else if (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) {
- parseFUNC(&reader, dw);
- } else if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) {
- parseSTRG(&reader, dw);
- } else if (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) {
- parseTXTR(&reader, dw, chunkEnd);
- } else if (options.parseAudo && memcmp(chunkName, "AUDO", 4) == 0) {
- parseAUDO(&reader, dw);
- } else {
- printf("Unknown chunk: %.4s (length %u at offset 0x%zX)\n", chunkName, chunkLength, chunkDataStart - 8);
- }
-
- // Free the chunk buffer and revert to FILE*-based reads for the next header
- if (chunkBuffer != nullptr) {
- BinaryReader_clearBuffer(&reader);
- free(chunkBuffer);
- }
-
- // Seek to chunk end (skip any unread data or trailing padding)
- fseek(reader.file, (long) chunkEnd, SEEK_SET);
- chunkIndex++;
- }
-
- // GMS2: apply default FPS to rooms with speed=0
- if (dw->gen8.gms2FPS > 0) {
- repeat(dw->room.count, i) {
- if (dw->room.rooms[i].speed == 0) {
- dw->room.rooms[i].speed = (uint32_t) dw->gen8.gms2FPS;
- }
- }
- }
-
- // If lazy-loading rooms, keep the file handle open for DataWin_loadRoomPayload, otherwise close it now
- dw->lazyLoadRooms = options.lazyLoadRooms;
- if (options.lazyLoadRooms) {
- dw->lazyLoadFile = file;
- dw->lazyLoadFilePath = safeStrdup(filePath);
- } else {
- dw->lazyLoadFile = nullptr;
- dw->lazyLoadFilePath = nullptr;
- fclose(file);
- }
-
- return dw;
-}
-
-// ===[ FREE ]===
-
-void DataWin_free(DataWin* dw) {
- if (!dw) return;
-
- // GEN8
- free(dw->gen8.roomOrder);
-
- // OPTN
- free(dw->optn.constants);
-
- // LANG
- free(dw->lang.entryIds);
- if (dw->lang.languages) {
- repeat(dw->lang.languageCount, i) {
- free(dw->lang.languages[i].entries);
- }
- free(dw->lang.languages);
- }
-
- // EXTN
- if (dw->extn.extensions) {
- repeat(dw->extn.count, i) {
- Extension* ext = &dw->extn.extensions[i];
- if (ext->files) {
- repeat(ext->fileCount, j) {
- ExtensionFile* file = &ext->files[j];
- if (file->functions) {
- repeat(file->functionCount, k) {
- free(file->functions[k].arguments);
- }
- free(file->functions);
- }
- }
- free(ext->files);
- }
- }
- free(dw->extn.extensions);
- }
-
- // SOND
- free(dw->sond.sounds);
-
- // AGRP
- free(dw->agrp.audioGroups);
-
- // SPRT
- if (dw->sprt.sprites) {
- repeat(dw->sprt.count, i) {
- free(dw->sprt.sprites[i].tpagIndices);
- if (dw->sprt.sprites[i].masks != nullptr) {
- repeat(dw->sprt.sprites[i].maskCount, j) {
- free(dw->sprt.sprites[i].masks[j]);
- }
- free(dw->sprt.sprites[i].masks);
- }
- // Runtime-allocated sprites (indices >= parsedCount) own their synthesized name
- if (i >= dw->sprt.parsedCount) free((char*) dw->sprt.sprites[i].name);
- }
- free(dw->sprt.sprites);
- }
-
-
- // BGND
- if (dw->bgnd.backgrounds) {
- repeat(dw->bgnd.count, i) {
- free(dw->bgnd.backgrounds[i].gms2TileIds);
- }
- }
- free(dw->bgnd.backgrounds);
-
- // PATH
- if (dw->path.paths) {
- repeat(dw->path.count, i) {
- free(dw->path.paths[i].points);
- free(dw->path.paths[i].internalPoints);
- }
- free(dw->path.paths);
- }
-
- // SCPT
- free(dw->scpt.scripts);
-
- // GLOB
- free(dw->glob.codeIds);
-
- // SHDR
- if (dw->shdr.shaders) {
- repeat(dw->shdr.count, i) {
- free(dw->shdr.shaders[i].vertexAttributes);
- }
- free(dw->shdr.shaders);
- }
-
- // FONT
- if (dw->font.fonts) {
- repeat(dw->font.count, i) {
- Font* font = &dw->font.fonts[i];
- if (font->glyphs) {
- repeat(font->glyphCount, j) {
- free(font->glyphs[j].kerning);
- }
- free(font->glyphs);
- }
- }
- free(dw->font.fonts);
- }
-
- // TMLN
- if (dw->tmln.timelines) {
- repeat(dw->tmln.count, i) {
- Timeline* tl = &dw->tmln.timelines[i];
- if (tl->moments) {
- repeat(tl->momentCount, j) {
- free(tl->moments[j].actions);
- }
- free(tl->moments);
- }
- }
- free(dw->tmln.timelines);
- }
-
- // OBJT
- if (dw->objt.objects) {
- repeat(dw->objt.count, i) {
- GameObject* obj = &dw->objt.objects[i];
- free(obj->physicsVertices);
- repeat(OBJT_EVENT_TYPE_COUNT, e) {
- ObjectEventList* list = &obj->eventLists[e];
- if (list->events) {
- repeat(list->eventCount, j) {
- free(list->events[j].actions);
- }
- free(list->events);
- }
- }
- }
- free(dw->objt.objects);
- }
-
- // ROOM
- if (dw->room.rooms) {
- repeat(dw->room.count, i) {
- DataWin_freeRoomPayload(&dw->room.rooms[i]);
- }
- free(dw->room.rooms);
- }
-
- // TPAG
- free(dw->tpag.items);
-
- // CODE
- free(dw->code.entries);
-
- // VARI
- free(dw->vari.variables);
-
- // FUNC
- free(dw->func.functions);
- if (dw->func.codeLocals) {
- repeat(dw->func.codeLocalsCount, i) {
- free(dw->func.codeLocals[i].locals);
- }
- free(dw->func.codeLocals);
- }
-
- // STRG
- free(dw->strg.strings);
-
- // TXTR
- if (dw->txtr.textures) {
- repeat(dw->txtr.count, i) {
- free(dw->txtr.textures[i].blobData);
- }
- free(dw->txtr.textures);
- }
-
- // AUDO
- if (dw->audo.entries) {
- repeat(dw->audo.count, i) {
- free(dw->audo.entries[i].data);
- }
- free(dw->audo.entries);
- }
-
- // Owned buffers
- free(dw->strgBuffer);
- free(dw->bytecodeBuffer);
-
- // Close the lazy-load file handle (only open when lazyLoadRooms was enabled)
- if (dw->lazyLoadFile != nullptr) {
- fclose(dw->lazyLoadFile);
- dw->lazyLoadFile = nullptr;
- }
- free(dw->lazyLoadFilePath);
-
- free(dw);
-}
-
-// ===[ Lazy Room Payload ]===
-
-void DataWin_freeRoomPayload(Room* room) {
- requireNotNull(room);
- free(room->backgrounds);
- room->backgrounds = nullptr;
- free(room->views);
- room->views = nullptr;
- free(room->gameObjects);
- room->gameObjects = nullptr;
- room->gameObjectCount = 0;
- free(room->tiles);
- room->tiles = nullptr;
- room->tileCount = 0;
- if (room->layerCount != 0 && room->layers != nullptr) {
- repeat(room->layerCount, j) {
- RoomLayer* layer = &room->layers[j];
- if (layer->assetsData) {
- free(layer->assetsData->legacyTiles);
- free(layer->assetsData->sprites);
- free(layer->assetsData);
- }
- if (layer->backgroundData) free(layer->backgroundData);
- if (layer->instancesData) {
- free(layer->instancesData->instanceIds);
- free(layer->instancesData);
- }
- if (layer->tilesData) {
- free(layer->tilesData->tileData);
- free(layer->tilesData);
- }
- }
- }
- free(room->layers);
- room->layers = nullptr;
- room->layerCount = 0;
- room->payloadLoaded = false;
-}
-
-void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex) {
- require(roomIndex >= 0 && dw->room.count > (uint32_t) roomIndex);
- Room* room = &dw->room.rooms[roomIndex];
- if (room->payloadLoaded) return;
- requireMessage(dw->lazyLoadFile != nullptr, "DataWin_loadRoomPayload called without an open lazy-load FILE*");
-
- FILE* f = dw->lazyLoadFile;
- // Find file size at lazy-load time (only needed for BinaryReader bounds checking).
- long savedPos = ftell(f);
- fseek(f, 0, SEEK_END);
- size_t fileSize = (size_t) ftell(f);
- fseek(f, savedPos, SEEK_SET);
-
- BinaryReader lazyReader = BinaryReader_create(f, fileSize);
- readRoomPayload(&lazyReader, dw, room);
-}
-
-// ===[ Dynamic Sprite Slot Allocation ]===
-
-uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex) {
- uint32_t newIndex;
- for (uint32_t i = startIndex; dw->sprt.count > i; i++) {
- if (dw->sprt.sprites[i].textureCount == 0) {
- newIndex = i;
- goto assignName;
- }
- }
- newIndex = dw->sprt.count;
- dw->sprt.count++;
- dw->sprt.sprites = safeRealloc(dw->sprt.sprites, dw->sprt.count * sizeof(Sprite));
- memset(&dw->sprt.sprites[newIndex], 0, sizeof(Sprite));
-assignName:
- // Match the native runner: set a "__newsprite" name so asset_get_index can find it.
- // A reused slot preserves its name across glDeleteSprite's memset, so we only strdup when the slot is freshly appended (name is still NULL).
- if (!dw->sprt.sprites[newIndex].name) {
- char buf[32];
- snprintf(buf, sizeof(buf), "__newsprite%u", newIndex);
- dw->sprt.sprites[newIndex].name = strdup(buf);
- }
- return newIndex;
-}
-
-// ===[ Version Detection ]===
-
-bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) {
- const DetectedFormat* f = &dw->detectedFormat;
- if (f->major != major) return f->major > major;
- if (f->minor != minor) return f->minor > minor;
- if (f->release != release) return f->release > release;
- return f->build >= build;
-}
-
-void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) {
- if (DataWin_isVersionAtLeast(dw, major, minor, release, build)) return;
- dw->detectedFormat.major = major;
- dw->detectedFormat.minor = minor;
- dw->detectedFormat.release = release;
- dw->detectedFormat.build = build;
-}
+#include "data_win.h"
+#include "binary_reader.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "stb_ds.h"
+#include "utils.h"
+
+// ===[ HELPERS ]===
+
+// Reads a uint32 absolute file offset, resolves it into the pre-loaded STRG buffer,
+// and returns a pointer to the null-terminated string content at that offset.
+static const char* readStringPtr(BinaryReader* reader, DataWin* dw) {
+ uint32_t offset = BinaryReader_readUint32(reader);
+ if (offset == 0) return nullptr;
+ return (const char*) (dw->strgBuffer + (offset - dw->strgBufferBase));
+}
+
+// Reads a pointer list header: count + absolute-offset pointers.
+// Caller must free the returned array.
+static uint32_t* readPointerTable(BinaryReader* reader, uint32_t* outCount) {
+ *outCount = BinaryReader_readUint32(reader);
+ if (*outCount == 0) return nullptr;
+ uint32_t* ptrs = safeMalloc(*outCount * sizeof(uint32_t));
+ repeat(*outCount, i) {
+ ptrs[i] = BinaryReader_readUint32(reader);
+ }
+ return ptrs;
+}
+
+// Reads a PointerList of EventAction entries. Used by TMLN and OBJT.
+static EventAction* readEventActions(BinaryReader* reader, DataWin* dw, uint32_t* outCount) {
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ *outCount = count;
+ if (count == 0) { free(ptrs); return nullptr; }
+
+ EventAction* actions = safeMalloc(count * sizeof(EventAction));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ actions[i].libID = BinaryReader_readUint32(reader);
+ actions[i].id = BinaryReader_readUint32(reader);
+ actions[i].kind = BinaryReader_readUint32(reader);
+ actions[i].useRelative = BinaryReader_readBool32(reader);
+ actions[i].isQuestion = BinaryReader_readBool32(reader);
+ actions[i].useApplyTo = BinaryReader_readBool32(reader);
+ actions[i].exeType = BinaryReader_readUint32(reader);
+ actions[i].actionName = readStringPtr(reader, dw);
+ actions[i].codeId = BinaryReader_readInt32(reader);
+ actions[i].argumentCount = BinaryReader_readUint32(reader);
+ actions[i].who = BinaryReader_readInt32(reader);
+ actions[i].relative = BinaryReader_readBool32(reader);
+ actions[i].isNot = BinaryReader_readBool32(reader);
+ actions[i].unknownAlwaysZero = BinaryReader_readUint32(reader);
+ }
+ free(ptrs);
+ return actions;
+}
+
+// ===[ PATH INTERNAL COMPUTATION ]===
+// Matches HTML5 yyPath.js algorithm exactly.
+
+// Dynamic array of InternalPathPoints for building during computation
+static InternalPathPoint* tempIntPoints = nullptr;
+static uint32_t tempIntPointCount = 0;
+
+static void addInternalPoint(float x, float y, float speed) {
+ InternalPathPoint pt = { .x = x, .y = y, .speed = speed, .l = 0.0 };
+ arrput(tempIntPoints, pt);
+ tempIntPointCount++;
+}
+
+// Recursive midpoint subdivision for smooth curves (yyPath.js:225-242)
+static void handlePiece(int depth, float x1, float y1, float s1, float x2, float y2, float s2, float x3, float y3, float s3) {
+ if (depth == 0) return;
+
+ float mx = (x1 + x2 + x2 + x3) / 4.0f;
+ float my = (y1 + y2 + y2 + y3) / 4.0f;
+ float ms = (s1 + s2 + s2 + s3) / 4.0f;
+
+ if ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) > 16.0f) {
+ handlePiece(depth - 1, x1, y1, s1, (x2 + x1) / 2.0f, (y2 + y1) / 2.0f, (s2 + s1) / 2.0f, mx, my, ms);
+ }
+
+ addInternalPoint(mx, my, ms);
+
+ if ((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3) > 16.0f) {
+ handlePiece(depth - 1, mx, my, ms, (x3 + x2) / 2.0f, (y3 + y2) / 2.0f, (s3 + s2) / 2.0f, x3, y3, s3);
+ }
+}
+
+void GamePath_computeInternal(GamePath* path) {
+ // Reset temp state
+ arrfree(tempIntPoints);
+ tempIntPoints = nullptr;
+ tempIntPointCount = 0;
+
+ free(path->internalPoints);
+ path->internalPoints = nullptr;
+ path->internalPointCount = 0;
+ path->length = 0.0;
+
+ if (path->pointCount == 0)
+ return;
+
+ if (path->isSmooth) {
+ // ComputeCurved (yyPath.js:254-292)
+ if (!path->isClosed) {
+ addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed);
+ }
+
+ int n;
+ if (path->isClosed) {
+ n = (int) path->pointCount - 1;
+ } else {
+ n = (int) path->pointCount - 3;
+ }
+
+ repeat(n + 1, i) {
+ PathPoint* p1 = &path->points[i % path->pointCount];
+ PathPoint* p2 = &path->points[(i + 1) % path->pointCount];
+ PathPoint* p3 = &path->points[(i + 2) % path->pointCount];
+ handlePiece((int) path->precision,
+ (p1->x + p2->x) / 2.0f, (p1->y + p2->y) / 2.0f, (p1->speed + p2->speed) / 2.0f,
+ p2->x, p2->y, p2->speed,
+ (p2->x + p3->x) / 2.0f, (p2->y + p3->y) / 2.0f, (p2->speed + p3->speed) / 2.0f);
+ }
+
+ if (!path->isClosed) {
+ PathPoint* last = &path->points[path->pointCount - 1];
+ addInternalPoint(last->x, last->y, last->speed);
+ } else {
+ // Closed smooth: append the first internal point again
+ addInternalPoint(tempIntPoints[0].x, tempIntPoints[0].y, tempIntPoints[0].speed);
+ }
+ } else {
+ // ComputeLinear (yyPath.js:192-204)
+ repeat(path->pointCount, i) {
+ addInternalPoint(path->points[i].x, path->points[i].y, path->points[i].speed);
+ }
+ if (path->isClosed) {
+ addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed);
+ }
+ }
+
+ // ComputeLength (yyPath.js:150-160)
+ path->internalPointCount = tempIntPointCount;
+ path->internalPoints = safeMalloc(tempIntPointCount * sizeof(InternalPathPoint));
+ memcpy(path->internalPoints, tempIntPoints, tempIntPointCount * sizeof(InternalPathPoint));
+ arrfree(tempIntPoints);
+ tempIntPoints = nullptr;
+ tempIntPointCount = 0;
+
+ path->length = 0.0;
+ if (path->internalPointCount > 0) {
+ path->internalPoints[0].l = 0.0;
+ repeat(path->internalPointCount - 1, j) {
+ uint32_t i = j + 1;
+ float dx = path->internalPoints[i].x - path->internalPoints[i - 1].x;
+ float dy = path->internalPoints[i].y - path->internalPoints[i - 1].y;
+ path->length += sqrtf(dx * dx + dy * dy);
+ path->internalPoints[i].l = path->length;
+ }
+ }
+}
+
+// Get interpolated position at t in [0,1] (yyPath.js:362-409)
+PathPositionResult GamePath_getPosition(GamePath* path, float t) {
+ PathPositionResult result = { .x = 0.0f, .y = 0.0f, .speed = 0.0f };
+
+ if (path->internalPointCount == 0) return result;
+
+ if (path->internalPointCount == 1 || path->length == 0.0f || 0.0f >= t) {
+ result.x = path->internalPoints[0].x;
+ result.y = path->internalPoints[0].y;
+ result.speed = path->internalPoints[0].speed;
+ return result;
+ }
+
+ if (t >= 1.0f) {
+ InternalPathPoint* last = &path->internalPoints[path->internalPointCount - 1];
+ result.x = last->x;
+ result.y = last->y;
+ result.speed = last->speed;
+ return result;
+ }
+
+ // Get the right interval via linear scan
+ float l = path->length * t;
+ uint32_t pos = 0;
+ while (path->internalPointCount - 2 > pos && l >= path->internalPoints[pos + 1].l) {
+ pos++;
+ }
+
+ InternalPathPoint* node = &path->internalPoints[pos];
+ float lRem = l - node->l;
+ float w = path->internalPoints[pos + 1].l - node->l;
+
+ if (w != 0.0f) {
+ InternalPathPoint* next = &path->internalPoints[pos + 1];
+ result.x = node->x + lRem * (next->x - node->x) / w;
+ result.y = node->y + lRem * (next->y - node->y) / w;
+ result.speed = node->speed + lRem * (next->speed - node->speed) / w;
+ } else {
+ result.x = node->x;
+ result.y = node->y;
+ result.speed = node->speed;
+ }
+
+ return result;
+}
+
+// ===[ CHUNK PARSERS ]===
+
+static void parseGEN8(BinaryReader* reader, DataWin* dw) {
+ Gen8* g = &dw->gen8;
+ g->isDebuggerDisabled = BinaryReader_readUint8(reader);
+ g->bytecodeVersion = BinaryReader_readUint8(reader);
+ BinaryReader_skip(reader, 2); // padding
+ g->fileName = readStringPtr(reader, dw);
+ g->config = readStringPtr(reader, dw);
+ g->lastObj = BinaryReader_readUint32(reader);
+ g->lastTile = BinaryReader_readUint32(reader);
+ g->gameID = BinaryReader_readUint32(reader);
+ BinaryReader_readBytes(reader, g->directPlayGuid, 16);
+ g->name = readStringPtr(reader, dw);
+ g->major = BinaryReader_readUint32(reader);
+ g->minor = BinaryReader_readUint32(reader);
+ g->release = BinaryReader_readUint32(reader);
+ g->build = BinaryReader_readUint32(reader);
+ g->defaultWindowWidth = BinaryReader_readUint32(reader);
+ g->defaultWindowHeight = BinaryReader_readUint32(reader);
+ g->info = BinaryReader_readUint32(reader);
+ g->licenseCRC32 = BinaryReader_readUint32(reader);
+ BinaryReader_readBytes(reader, g->licenseMD5, 16);
+ g->timestamp = BinaryReader_readUint64(reader);
+ g->displayName = readStringPtr(reader, dw);
+ g->activeTargets = BinaryReader_readUint64(reader);
+ g->functionClassifications = BinaryReader_readUint64(reader);
+ g->steamAppID = BinaryReader_readInt32(reader);
+ if (g->bytecodeVersion >= 14) {
+ g->debuggerPort = BinaryReader_readUint32(reader);
+ }
+
+ // Room order SimpleList
+ g->roomOrderCount = BinaryReader_readUint32(reader);
+ if (g->roomOrderCount > 0) {
+ g->roomOrder = safeMalloc(g->roomOrderCount * sizeof(int32_t));
+ repeat(g->roomOrderCount, i) {
+ g->roomOrder[i] = BinaryReader_readInt32(reader);
+ }
+ } else {
+ g->roomOrder = nullptr;
+ }
+
+ if (g->major >= 2) {
+ BinaryReader_skip(reader, 8); // firstRandom (int64)
+ BinaryReader_skip(reader, 8*4); // 4 Random Entries (one int64 or two int32)
+
+ g->gms2FPS = BinaryReader_readFloat32(reader);
+ BinaryReader_skip(reader, 4); // AllowStatistics (bool32)
+ BinaryReader_skip(reader, 16); // GameGUID (16 Bytes, unknown it's use)
+ }
+
+ // Seed the detected version from GEN8.
+ // Later chunk parsers may bump these upward when they identify newer-format features, because since GM:S 2 the value in the GEN8 chunk is not accurate.
+ DataWin_bumpVersionTo(dw, g->major, g->minor, g->release, g->build);
+}
+
+static void parseOPTN(BinaryReader* reader, DataWin* dw) {
+ Optn* o = &dw->optn;
+
+ int32_t marker = BinaryReader_readInt32(reader);
+ if (marker != (int32_t)0x80000000) {
+ fprintf(stderr, "OPTN: expected new format marker 0x80000000, got 0x%08X\n", (uint32_t)marker);
+ exit(1);
+ }
+
+ int32_t shaderExtVersion = BinaryReader_readInt32(reader);
+ (void)shaderExtVersion; // always 2
+
+ o->info = BinaryReader_readUint64(reader);
+ o->scale = BinaryReader_readInt32(reader);
+ o->windowColor = BinaryReader_readUint32(reader);
+ o->colorDepth = BinaryReader_readUint32(reader);
+ o->resolution = BinaryReader_readUint32(reader);
+ o->frequency = BinaryReader_readUint32(reader);
+ o->vertexSync = BinaryReader_readUint32(reader);
+ o->priority = BinaryReader_readUint32(reader);
+ o->backImage = BinaryReader_readUint32(reader);
+ o->frontImage = BinaryReader_readUint32(reader);
+ o->loadImage = BinaryReader_readUint32(reader);
+ o->loadAlpha = BinaryReader_readUint32(reader);
+
+ // Constants SimpleList
+ o->constantCount = BinaryReader_readUint32(reader);
+ if (o->constantCount > 0) {
+ o->constants = safeMalloc(o->constantCount * sizeof(OptnConstant));
+ repeat(o->constantCount, i) {
+ o->constants[i].name = readStringPtr(reader, dw);
+ o->constants[i].value = readStringPtr(reader, dw);
+ }
+ } else {
+ o->constants = nullptr;
+ }
+}
+
+static void parseLANG(BinaryReader* reader, DataWin* dw) {
+ Lang* l = &dw->lang;
+ l->unknown1 = BinaryReader_readUint32(reader);
+ l->languageCount = BinaryReader_readUint32(reader);
+ l->entryCount = BinaryReader_readUint32(reader);
+
+ // Entry IDs
+ if (l->entryCount > 0) {
+ l->entryIds = safeMalloc(l->entryCount * sizeof(const char*));
+ repeat(l->entryCount, i) {
+ l->entryIds[i] = readStringPtr(reader, dw);
+ }
+ } else {
+ l->entryIds = nullptr;
+ }
+
+ // Languages
+ if (l->languageCount > 0) {
+ l->languages = safeMalloc(l->languageCount * sizeof(Language));
+ repeat(l->languageCount, i) {
+ l->languages[i].name = readStringPtr(reader, dw);
+ l->languages[i].region = readStringPtr(reader, dw);
+ l->languages[i].entryCount = l->entryCount;
+ if (l->entryCount > 0) {
+ l->languages[i].entries = safeMalloc(l->entryCount * sizeof(const char*));
+ repeat(l->entryCount, j) {
+ l->languages[i].entries[j] = readStringPtr(reader, dw);
+ }
+ } else {
+ l->languages[i].entries = nullptr;
+ }
+ }
+ } else {
+ l->languages = nullptr;
+ }
+}
+
+static void parseEXTN(BinaryReader* reader, DataWin* dw) {
+ // TODO: Update EXTN parser because it is broken for newer GM:S 2 versions
+ Extn* e = &dw->extn;
+
+ uint32_t extCount;
+ uint32_t* extPtrs = readPointerTable(reader, &extCount);
+ e->count = extCount;
+
+ if (extCount == 0) { free(extPtrs); e->extensions = nullptr; return; }
+
+ e->extensions = safeMalloc(extCount * sizeof(Extension));
+ repeat(extCount, i) {
+ BinaryReader_seek(reader, extPtrs[i]);
+ Extension* ext = &e->extensions[i];
+ ext->folderName = readStringPtr(reader, dw);
+ ext->name = readStringPtr(reader, dw);
+ ext->className = readStringPtr(reader, dw);
+
+ // Files PointerList
+ uint32_t fileCount;
+ uint32_t* filePtrs = readPointerTable(reader, &fileCount);
+ ext->fileCount = fileCount;
+
+ if (fileCount > 0) {
+ ext->files = safeMalloc(fileCount * sizeof(ExtensionFile));
+ repeat(fileCount, j) {
+ BinaryReader_seek(reader, filePtrs[j]);
+ ExtensionFile* file = &ext->files[j];
+ file->filename = readStringPtr(reader, dw);
+ file->cleanupScript = readStringPtr(reader, dw);
+ file->initScript = readStringPtr(reader, dw);
+ file->kind = BinaryReader_readUint32(reader);
+
+ // Functions PointerList
+ uint32_t funcCount;
+ uint32_t* funcPtrs = readPointerTable(reader, &funcCount);
+ file->functionCount = funcCount;
+
+ if (funcCount > 0) {
+ file->functions = safeMalloc(funcCount * sizeof(ExtensionFunction));
+ repeat(funcCount, k) {
+ BinaryReader_seek(reader, funcPtrs[k]);
+ ExtensionFunction* func = &file->functions[k];
+ func->name = readStringPtr(reader, dw);
+ func->id = BinaryReader_readUint32(reader);
+ func->kind = BinaryReader_readUint32(reader);
+ func->retType = BinaryReader_readUint32(reader);
+ func->extName = readStringPtr(reader, dw);
+
+ // Arguments SimpleList
+ func->argumentCount = BinaryReader_readUint32(reader);
+ if (func->argumentCount > 0) {
+ func->arguments = safeMalloc(func->argumentCount * sizeof(uint32_t));
+ repeat(func->argumentCount, a) {
+ func->arguments[a] = BinaryReader_readUint32(reader);
+ }
+ } else {
+ func->arguments = nullptr;
+ }
+ }
+ } else {
+ file->functions = nullptr;
+ }
+ free(funcPtrs);
+ }
+ } else {
+ ext->files = nullptr;
+ }
+ free(filePtrs);
+ }
+ free(extPtrs);
+
+ // Product ID data (16 bytes per extension, bytecodeVersion >= 14)
+ // Skipped -- we seek to chunkEnd after parsing
+}
+
+static void parseSOND(BinaryReader* reader, DataWin* dw) {
+ Sond* s = &dw->sond;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ s->count = count;
+
+ if (count == 0) { free(ptrs); s->sounds = nullptr; return; }
+
+ s->sounds = safeMalloc(count * sizeof(Sound));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ Sound* snd = &s->sounds[i];
+ snd->name = readStringPtr(reader, dw);
+ snd->flags = BinaryReader_readUint32(reader);
+ snd->type = readStringPtr(reader, dw);
+ snd->file = readStringPtr(reader, dw);
+ snd->effects = BinaryReader_readUint32(reader);
+ snd->volume = BinaryReader_readFloat32(reader);
+ snd->pitch = BinaryReader_readFloat32(reader);
+
+ // AudioGroup or preload field at offset +28
+ // For GMS 1.4.x (bytecodeVersion >= 14) with Regular flag: resource_id
+ if ((snd->flags & 0x64) == 0x64) {
+ snd->audioGroup = BinaryReader_readInt32(reader);
+ } else {
+ int32_t preload = BinaryReader_readInt32(reader);
+ (void)preload;
+ snd->audioGroup = 0; // default audio group
+ }
+
+ snd->audioFile = BinaryReader_readInt32(reader);
+ }
+ free(ptrs);
+}
+
+static void parseAGRP(BinaryReader* reader, DataWin* dw) {
+ Agrp* a = &dw->agrp;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ a->count = count;
+
+ if (count == 0) { free(ptrs); a->audioGroups = nullptr; return; }
+
+ a->audioGroups = safeMalloc(count * sizeof(AudioGroup));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ a->audioGroups[i].name = readStringPtr(reader, dw);
+ }
+ free(ptrs);
+}
+
+static void parseSPRT(BinaryReader* reader, DataWin* dw, bool skipLoadingPreciseMasksForNonPreciseSprites) {
+ Sprt* s = &dw->sprt;
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ s->count = count;
+ s->parsedCount = count;
+
+ if (count == 0) { free(ptrs); s->sprites = nullptr; return; }
+
+ s->sprites = safeCalloc(count, sizeof(Sprite));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ Sprite* spr = &s->sprites[i];
+ spr->name = readStringPtr(reader, dw);
+ spr->width = BinaryReader_readUint32(reader);
+ spr->height = BinaryReader_readUint32(reader);
+ spr->marginLeft = BinaryReader_readInt32(reader);
+ spr->marginRight = BinaryReader_readInt32(reader);
+ spr->marginBottom = BinaryReader_readInt32(reader);
+ spr->marginTop = BinaryReader_readInt32(reader);
+ spr->transparent = BinaryReader_readBool32(reader);
+ spr->smooth = BinaryReader_readBool32(reader);
+ spr->preload = BinaryReader_readBool32(reader);
+ spr->bboxMode = BinaryReader_readUint32(reader);
+ spr->sepMasks = BinaryReader_readUint32(reader);
+ spr->originX = BinaryReader_readInt32(reader);
+ spr->originY = BinaryReader_readInt32(reader);
+
+ // Detect special type vs normal: peek next int32
+ int32_t check = BinaryReader_readInt32(reader);
+ uint32_t nineSliceOffset = 0;
+ if (check == -1) {
+ spr->specialType = true;
+ spr->sVersion = BinaryReader_readUint32(reader);
+ spr->sSpriteType = BinaryReader_readUint32(reader);
+ if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) {
+ spr->gms2PlaybackSpeed = BinaryReader_readFloat32(reader);
+ spr->gms2PlaybackSpeedType = BinaryReader_readUint32(reader);
+ if (spr->sVersion >= 2) {
+ BinaryReader_skip(reader, 4); //sequenceOffset;
+ if (spr->sVersion >= 3) {
+ nineSliceOffset = BinaryReader_readUint32(reader);
+ }
+ }
+ check = BinaryReader_readUint32(reader);
+ }
+ }
+
+ // 'check' is the texture count (start of SimpleList)
+ spr->textureCount = (uint32_t)check;
+ if (spr->textureCount > 0) {
+ // Temporarily store the absolute file offsets here; parseTPAG resolves them in-place to TPAG indices once the TPAG table is known.
+ spr->tpagIndices = safeMalloc(spr->textureCount * sizeof(int32_t));
+ repeat(spr->textureCount, j) {
+ spr->tpagIndices[j] = (int32_t) BinaryReader_readUint32(reader);
+ }
+ } else {
+ spr->tpagIndices = nullptr;
+ }
+
+ // Collision mask data
+ // sepMasks: 0 = axis-aligned rect (no mask data stored in some cases)
+ // 1 = precise per-frame masks
+ // 2 = rotated rect (no mask data)
+ // Mask format: each bit = 1 pixel, MSB first, row-major
+ // Width in bytes = (spriteWidth + 7) / 8, total = widthInBytes * spriteHeight
+ // After all masks, data is padded to 4-byte alignment
+ uint32_t maskDataCount = BinaryReader_readUint32(reader);
+ spr->maskCount = maskDataCount;
+ if (maskDataCount > 0 && spr->width > 0 && spr->height > 0) {
+ uint32_t bytesPerRow = (spr->width + 7) / 8;
+ uint32_t bytesPerMask = bytesPerRow * spr->height;
+
+ if (spr->sepMasks == 1 || !skipLoadingPreciseMasksForNonPreciseSprites) {
+ spr->masks = safeMalloc(maskDataCount * sizeof(uint8_t*));
+ repeat(maskDataCount, j) {
+ spr->masks[j] = safeMalloc(bytesPerMask);
+ BinaryReader_readBytes(reader, spr->masks[j], bytesPerMask);
+ }
+ } else {
+ BinaryReader_skip(reader, bytesPerMask * maskDataCount);
+ spr->masks = nullptr;
+ }
+ // Pad the TOTAL mask data to 4-byte alignment (not per-mask)
+ uint32_t totalMaskBytes = bytesPerMask * maskDataCount;
+ uint32_t remainder = totalMaskBytes % 4;
+ if (remainder != 0) {
+ BinaryReader_skip(reader, 4 - remainder);
+ }
+ } else {
+ spr->masks = nullptr;
+ }
+
+ // Nine-slice block (40 bytes). Located at nineSliceOffset (absolute file offset) elsewhere in the chunk.
+ if (nineSliceOffset != 0) {
+ size_t savedPos = BinaryReader_getPosition(reader);
+ BinaryReader_seek(reader, (size_t) nineSliceOffset);
+ spr->nsLeft = BinaryReader_readInt32(reader);
+ spr->nsTop = BinaryReader_readInt32(reader);
+ spr->nsRight = BinaryReader_readInt32(reader);
+ spr->nsBottom = BinaryReader_readInt32(reader);
+ spr->nineSliceEnabled = BinaryReader_readBool32(reader);
+ repeat(5, j) {
+ int32_t mode = BinaryReader_readInt32(reader);
+ spr->nsTileModes[j] = (uint8_t) mode;
+ }
+ BinaryReader_seek(reader, savedPos);
+ }
+ }
+
+ free(ptrs);
+}
+
+static void parseBGND(BinaryReader* reader, DataWin* dw) {
+ Bgnd* b = &dw->bgnd;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ b->count = count;
+
+ if (count == 0) { free(ptrs); b->backgrounds = nullptr; return; }
+
+ b->backgrounds = safeCalloc(count, sizeof(Background));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ Background* bg = &b->backgrounds[i];
+ bg->name = readStringPtr(reader, dw);
+ bg->transparent = BinaryReader_readBool32(reader);
+ bg->smooth = BinaryReader_readBool32(reader);
+ bg->preload = BinaryReader_readBool32(reader);
+ // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known.
+ bg->tpagIndex = (int32_t) BinaryReader_readUint32(reader);
+ if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) {
+ bg->gms2UnknownAlways2 = BinaryReader_readUint32(reader);
+ bg->gms2TileWidth = BinaryReader_readUint32(reader);
+ bg->gms2TileHeight = BinaryReader_readUint32(reader);
+ if (DataWin_isVersionAtLeast(dw, 2024, 14, 0, 1)) {
+ bg->gms2TileSeparationX = BinaryReader_readUint32(reader);
+ bg->gms2TileSeparationY = BinaryReader_readUint32(reader);
+ }
+ bg->gms2OutputBorderX = BinaryReader_readUint32(reader);
+ bg->gms2OutputBorderY = BinaryReader_readUint32(reader);
+ bg->gms2TileColumns = BinaryReader_readUint32(reader);
+ bg->gms2ItemsPerTileCount = BinaryReader_readUint32(reader);
+ bg->gms2TileCount = BinaryReader_readUint32(reader);
+ bg->gms2ExportedSpriteIndex = BinaryReader_readInt32(reader);
+ bg->gms2FrameLength = BinaryReader_readInt64(reader);
+ int tileIdCount = bg->gms2TileCount * bg->gms2ItemsPerTileCount;
+ bg->gms2TileIds = malloc(tileIdCount*sizeof(uint32_t));
+ repeat(tileIdCount, j) {
+ bg->gms2TileIds[j] = BinaryReader_readUint32(reader);
+ }
+ }
+ }
+ free(ptrs);
+}
+
+static void parsePATH(BinaryReader* reader, DataWin* dw) {
+ PathChunk* p = &dw->path;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ p->count = count;
+
+ if (count == 0) { free(ptrs); p->paths = nullptr; return; }
+
+ p->paths = safeMalloc(count * sizeof(GamePath));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ GamePath* path = &p->paths[i];
+ path->internalPoints = nullptr;
+ path->internalPointCount = 0;
+ path->length = 0.0;
+ path->name = readStringPtr(reader, dw);
+ path->isSmooth = BinaryReader_readBool32(reader);
+ path->isClosed = BinaryReader_readBool32(reader);
+ path->precision = BinaryReader_readUint32(reader);
+
+ // Points SimpleList
+ path->pointCount = BinaryReader_readUint32(reader);
+ if (path->pointCount > 0) {
+ path->points = safeMalloc(path->pointCount * sizeof(PathPoint));
+ repeat(path->pointCount, j) {
+ path->points[j].x = BinaryReader_readFloat32(reader);
+ path->points[j].y = BinaryReader_readFloat32(reader);
+ path->points[j].speed = BinaryReader_readFloat32(reader);
+ }
+ } else {
+ path->points = nullptr;
+ }
+
+ // Precompute internal representation for path following
+ GamePath_computeInternal(path);
+ }
+ free(ptrs);
+}
+
+static void parseSCPT(BinaryReader* reader, DataWin* dw) {
+ Scpt* s = &dw->scpt;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ s->count = count;
+
+ if (count == 0) { free(ptrs); s->scripts = nullptr; return; }
+
+ s->scripts = safeMalloc(count * sizeof(Script));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ s->scripts[i].name = readStringPtr(reader, dw);
+ s->scripts[i].codeId = BinaryReader_readInt32(reader);
+ }
+ free(ptrs);
+}
+
+static void parseGLOB(BinaryReader* reader, DataWin* dw) {
+ Glob* g = &dw->glob;
+
+ g->count = BinaryReader_readUint32(reader);
+ if (g->count > 0) {
+ g->codeIds = safeMalloc(g->count * sizeof(int32_t));
+ repeat(g->count, i) {
+ g->codeIds[i] = BinaryReader_readInt32(reader);
+ }
+ } else {
+ g->codeIds = nullptr;
+ }
+}
+
+static void parseSHDR(BinaryReader* reader, DataWin* dw) {
+ Shdr* s = &dw->shdr;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ s->count = count;
+
+ if (count == 0) { free(ptrs); s->shaders = nullptr; return; }
+
+ s->shaders = safeMalloc(count * sizeof(Shader));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ Shader* sh = &s->shaders[i];
+ sh->name = readStringPtr(reader, dw);
+ sh->type = BinaryReader_readUint32(reader) & 0x7FFFFFFF;
+ sh->glslES_Vertex = readStringPtr(reader, dw);
+ sh->glslES_Fragment = readStringPtr(reader, dw);
+ sh->glsl_Vertex = readStringPtr(reader, dw);
+ sh->glsl_Fragment = readStringPtr(reader, dw);
+ sh->hlsl9_Vertex = readStringPtr(reader, dw);
+ sh->hlsl9_Fragment = readStringPtr(reader, dw);
+ sh->hlsl11_VertexOffset = BinaryReader_readUint32(reader);
+ sh->hlsl11_PixelOffset = BinaryReader_readUint32(reader);
+
+ // Vertex attributes SimpleList
+ sh->vertexAttributeCount = BinaryReader_readUint32(reader);
+ if (sh->vertexAttributeCount > 0) {
+ sh->vertexAttributes = safeMalloc(sh->vertexAttributeCount * sizeof(const char*));
+ repeat(sh->vertexAttributeCount, j) {
+ sh->vertexAttributes[j] = readStringPtr(reader, dw);
+ }
+ } else {
+ sh->vertexAttributes = nullptr;
+ }
+
+ // Version field (bytecodeVersion > 13)
+ sh->version = BinaryReader_readInt32(reader);
+
+ sh->pssl_VertexOffset = BinaryReader_readUint32(reader);
+ sh->pssl_VertexLen = BinaryReader_readUint32(reader);
+ sh->pssl_PixelOffset = BinaryReader_readUint32(reader);
+ sh->pssl_PixelLen = BinaryReader_readUint32(reader);
+ sh->cgVita_VertexOffset = BinaryReader_readUint32(reader);
+ sh->cgVita_VertexLen = BinaryReader_readUint32(reader);
+ sh->cgVita_PixelOffset = BinaryReader_readUint32(reader);
+ sh->cgVita_PixelLen = BinaryReader_readUint32(reader);
+
+ if (sh->version >= 2) {
+ sh->cgPS3_VertexOffset = BinaryReader_readUint32(reader);
+ sh->cgPS3_VertexLen = BinaryReader_readUint32(reader);
+ sh->cgPS3_PixelOffset = BinaryReader_readUint32(reader);
+ sh->cgPS3_PixelLen = BinaryReader_readUint32(reader);
+ } else {
+ sh->cgPS3_VertexOffset = 0;
+ sh->cgPS3_VertexLen = 0;
+ sh->cgPS3_PixelOffset = 0;
+ sh->cgPS3_PixelLen = 0;
+ }
+
+ // Blob data follows but we skip it (pointer list seeking handles position)
+ }
+ free(ptrs);
+}
+
+static void parseFONT(BinaryReader* reader, DataWin* dw) {
+ FontChunk* f = &dw->font;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ f->count = count;
+
+ if (count == 0) { free(ptrs); f->fonts = nullptr; return; }
+
+ // We need to figure out how many uint32 fields are between here and the PointerList
+ uint32_t fontOptionalCount = (dw->gen8.bytecodeVersion >= 17) ? 1u : 0u;
+ {
+ size_t baseAfterScaleY = (size_t) ptrs[0] + 40;
+ for (uint32_t trial = fontOptionalCount; 4 >= trial; trial++) {
+ size_t listStart = baseAfterScaleY + 4u * trial;
+ BinaryReader_seek(reader, listStart);
+ uint32_t probedGlyphCount = BinaryReader_readUint32(reader);
+ if (probedGlyphCount == 0 || probedGlyphCount > 0x10000) continue;
+ uint32_t probedFirstPtr = BinaryReader_readUint32(reader);
+ size_t expectedFirstPtr = listStart + 4u + 4u * probedGlyphCount;
+ if ((size_t) probedFirstPtr == expectedFirstPtr) {
+ fontOptionalCount = trial;
+ break;
+ }
+ }
+ }
+
+ f->fonts = safeMalloc(count * sizeof(Font));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ Font* font = &f->fonts[i];
+ font->name = readStringPtr(reader, dw);
+ font->displayName = readStringPtr(reader, dw);
+ font->emSize = BinaryReader_readUint32(reader);
+ font->bold = BinaryReader_readBool32(reader);
+ font->italic = BinaryReader_readBool32(reader);
+ font->rangeStart = BinaryReader_readUint16(reader);
+ font->charset = BinaryReader_readUint8(reader);
+ font->antiAliasing = BinaryReader_readUint8(reader);
+ font->rangeEnd = BinaryReader_readUint32(reader);
+ // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known.
+ font->tpagIndex = (int32_t) BinaryReader_readUint32(reader);
+ font->scaleX = BinaryReader_readFloat32(reader);
+ font->scaleY = BinaryReader_readFloat32(reader);
+ // Optional fields appear in this order when present: AscenderOffset (BC17+),
+ // Ascender, SDFSpread, LineHeight. `fontOptionalCount` says how many are actually on disk.
+ font->ascenderOffset = 0;
+ font->ascender = 0;
+ font->sdfSpread = 0;
+ font->lineHeight = 0;
+ font->hasAscender = false;
+ font->hasSDFSpread = false;
+ font->hasLineHeight = false;
+ uint32_t readSoFar = 0;
+ if (dw->gen8.bytecodeVersion >= 17 && fontOptionalCount > readSoFar) {
+ font->ascenderOffset = BinaryReader_readInt32(reader);
+ readSoFar++;
+ }
+ if (fontOptionalCount > readSoFar) {
+ font->ascender = BinaryReader_readUint32(reader);
+ font->hasAscender = true;
+ readSoFar++;
+ }
+ if (fontOptionalCount > readSoFar) {
+ font->sdfSpread = BinaryReader_readUint32(reader);
+ font->hasSDFSpread = true;
+ readSoFar++;
+ }
+ if (fontOptionalCount > readSoFar) {
+ font->lineHeight = BinaryReader_readUint32(reader);
+ font->hasLineHeight = true;
+ readSoFar++;
+ }
+ font->isSpriteFont = false;
+ font->spriteIndex = -1;
+
+ // Glyphs PointerList
+ uint32_t glyphCount;
+ uint32_t* glyphPtrs = readPointerTable(reader, &glyphCount);
+ font->glyphCount = glyphCount;
+
+ uint32_t maxGlyphHeight = 0;
+ if (glyphCount > 0) {
+ font->glyphs = safeMalloc(glyphCount * sizeof(FontGlyph));
+ repeat(glyphCount, j) {
+ BinaryReader_seek(reader, glyphPtrs[j]);
+ FontGlyph* glyph = &font->glyphs[j];
+ glyph->character = BinaryReader_readUint16(reader);
+ glyph->sourceX = BinaryReader_readUint16(reader);
+ glyph->sourceY = BinaryReader_readUint16(reader);
+ glyph->sourceWidth = BinaryReader_readUint16(reader);
+ glyph->sourceHeight = BinaryReader_readUint16(reader);
+ glyph->shift = BinaryReader_readInt16(reader);
+ glyph->offset = BinaryReader_readInt16(reader);
+
+ if (glyph->sourceHeight > maxGlyphHeight) maxGlyphHeight = glyph->sourceHeight;
+
+ // Kerning SimpleListShort (uint16 count)
+ glyph->kerningCount = BinaryReader_readUint16(reader);
+ if (glyph->kerningCount > 0) {
+ glyph->kerning = safeMalloc(glyph->kerningCount * sizeof(KerningPair));
+ for (uint16_t k = 0; glyph->kerningCount > k; k++) {
+ glyph->kerning[k].character = BinaryReader_readInt16(reader);
+ glyph->kerning[k].shiftModifier = BinaryReader_readInt16(reader);
+ }
+ } else {
+ glyph->kerning = nullptr;
+ }
+ }
+ } else {
+ font->glyphs = nullptr;
+ }
+ font->maxGlyphHeight = maxGlyphHeight;
+ Font_buildGlyphLUT(font);
+ free(glyphPtrs);
+ }
+ free(ptrs);
+
+ // 512 bytes of trailing padding -- skipped by chunkEnd seek
+}
+
+static void parseTMLN(BinaryReader* reader, DataWin* dw) {
+ Tmln* t = &dw->tmln;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ t->count = count;
+
+ if (count == 0) { free(ptrs); t->timelines = nullptr; return; }
+
+ t->timelines = safeMalloc(count * sizeof(Timeline));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ Timeline* tl = &t->timelines[i];
+ tl->name = readStringPtr(reader, dw);
+ tl->momentCount = BinaryReader_readUint32(reader);
+
+ if (tl->momentCount > 0) {
+ tl->moments = safeMalloc(tl->momentCount * sizeof(TimelineMoment));
+
+ // Pass 1: Read step + event pointer pairs
+ uint32_t* eventPtrs = safeMalloc(tl->momentCount * sizeof(uint32_t));
+ repeat(tl->momentCount, j) {
+ tl->moments[j].step = BinaryReader_readUint32(reader);
+ eventPtrs[j] = BinaryReader_readUint32(reader);
+ }
+
+ // Pass 2: Parse event action lists
+ repeat(tl->momentCount, j) {
+ BinaryReader_seek(reader, eventPtrs[j]);
+ tl->moments[j].actions = readEventActions(reader, dw, &tl->moments[j].actionCount);
+ }
+ free(eventPtrs);
+ } else {
+ tl->moments = nullptr;
+ }
+ }
+ free(ptrs);
+}
+
+static void parseOBJT(BinaryReader* reader, DataWin* dw) {
+ Objt* o = &dw->objt;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ o->count = count;
+
+ if (count == 0) { free(ptrs); o->objects = nullptr; return; }
+
+ // Detect GMS 2022.5+ by probing the first game object's event list structure.
+ if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) {
+ // Skip the 16 fixed uint32 header fields (name..angularDamping) to reach physicsVertexCount.
+ BinaryReader_seek(reader, ptrs[0] + 16 * 4);
+ int32_t vertexCount = BinaryReader_readInt32(reader);
+ if (vertexCount >= 0) {
+ // Skip friction + awake + kinematic (12 bytes) and physics vertices (8 bytes each).
+ uint32_t skipCount = 12 + vertexCount * 8;
+ uint32_t newLocation = reader->bufferPos + skipCount;
+ bool isOldFormat = false;
+ if (newLocation < reader->bufferSize) {
+ BinaryReader_skip(reader, skipCount);
+ uint32_t eventTypeCount = BinaryReader_readUint32(reader);
+ if (eventTypeCount == OBJT_EVENT_TYPE_COUNT) {
+ uint32_t firstSubEventPtr = BinaryReader_readUint32(reader);
+ uint32_t currentAbsPos = (uint32_t) BinaryReader_getPosition(reader);
+ // The remaining 14 outer-list pointers sit between here and the first sub-event list.
+ if (firstSubEventPtr == currentAbsPos + 14 * 4) {
+ isOldFormat = true;
+ }
+ }
+ }
+ if (!isOldFormat) {
+ DataWin_bumpVersionTo(dw, 2022, 5, 0, 0);
+ }
+ }
+ }
+
+ o->objects = safeMalloc(count * sizeof(GameObject));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ GameObject* obj = &o->objects[i];
+ obj->name = readStringPtr(reader, dw);
+ obj->spriteId = BinaryReader_readInt32(reader);
+ obj->visible = BinaryReader_readBool32(reader);
+ if (DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) {
+ obj->managed = BinaryReader_readBool32(reader);
+ } else {
+ obj->managed = false;
+ }
+ obj->solid = BinaryReader_readBool32(reader);
+ obj->depth = BinaryReader_readInt32(reader);
+ obj->persistent = BinaryReader_readBool32(reader);
+ obj->parentId = BinaryReader_readInt32(reader);
+ obj->textureMaskId = BinaryReader_readInt32(reader);
+ obj->usesPhysics = BinaryReader_readBool32(reader);
+ obj->isSensor = BinaryReader_readBool32(reader);
+ obj->collisionShape = BinaryReader_readUint32(reader);
+ obj->density = BinaryReader_readFloat32(reader);
+ obj->restitution = BinaryReader_readFloat32(reader);
+ obj->group = BinaryReader_readUint32(reader);
+ obj->linearDamping = BinaryReader_readFloat32(reader);
+ obj->angularDamping = BinaryReader_readFloat32(reader);
+ obj->physicsVertexCount = BinaryReader_readInt32(reader);
+ obj->friction = BinaryReader_readFloat32(reader);
+ obj->awake = BinaryReader_readBool32(reader);
+ obj->kinematic = BinaryReader_readBool32(reader);
+
+ // Physics vertices
+ if (obj->physicsVertexCount > 0) {
+ obj->physicsVertices = safeMalloc(obj->physicsVertexCount * sizeof(PhysicsVertex));
+ for (int32_t j = 0; obj->physicsVertexCount > j; j++) {
+ obj->physicsVertices[j].x = BinaryReader_readFloat32(reader);
+ obj->physicsVertices[j].y = BinaryReader_readFloat32(reader);
+ }
+ } else {
+ obj->physicsVertices = nullptr;
+ }
+
+ // Events: UndertalePointerList>
+ // Outer pointer list: one entry per event type
+ // Inner pointer list: events for that type
+ uint32_t eventTypeCount;
+ uint32_t* eventTypePtrs = readPointerTable(reader, &eventTypeCount);
+
+ for (uint32_t eventType = 0; eventTypeCount > eventType && OBJT_EVENT_TYPE_COUNT > eventType; eventType++) {
+ BinaryReader_seek(reader, eventTypePtrs[eventType]);
+
+ // Inner pointer list: events for this type
+ uint32_t eventCount;
+ uint32_t* eventPtrs = readPointerTable(reader, &eventCount);
+
+ obj->eventLists[eventType].eventCount = eventCount;
+
+ if (eventCount > 0) {
+ obj->eventLists[eventType].events = safeMalloc(eventCount * sizeof(ObjectEvent));
+ repeat(eventCount, j) {
+ BinaryReader_seek(reader, eventPtrs[j]);
+ obj->eventLists[eventType].events[j].eventSubtype = BinaryReader_readUint32(reader);
+ obj->eventLists[eventType].events[j].actions = readEventActions(reader, dw, &obj->eventLists[eventType].events[j].actionCount);
+ }
+ } else {
+ obj->eventLists[eventType].events = nullptr;
+ }
+
+ free(eventPtrs);
+ }
+
+ // Zero-fill any unused event type slots
+ for (uint32_t eventType = eventTypeCount; OBJT_EVENT_TYPE_COUNT > eventType; eventType++) {
+ obj->eventLists[eventType].eventCount = 0;
+ obj->eventLists[eventType].events = nullptr;
+ }
+
+ free(eventTypePtrs);
+ }
+ free(ptrs);
+}
+
+// ===[ Room payload parsing helpers ]===
+// Each of these assumes the caller has seeked to the start of the relevant PointerList (where the uint32 "count" of the list is).
+// They allocate and populate the corresponding fields on Room.
+// They are used by both the eager parse path and the lazy load path (DataWin_loadRoomPayload).
+
+static void readRoomBackgrounds(BinaryReader* reader, Room* room) {
+ uint32_t bgCount;
+ uint32_t* bgPtrs = readPointerTable(reader, &bgCount);
+ room->backgrounds = safeMalloc(8 * sizeof(RoomBackground));
+ uint32_t fillEnd = bgCount < 8 ? bgCount : 8;
+ for (uint32_t j = 0; fillEnd > j; j++) {
+ BinaryReader_seek(reader, bgPtrs[j]);
+ RoomBackground* bg = &room->backgrounds[j];
+ bg->enabled = BinaryReader_readBool32(reader);
+ bg->foreground = BinaryReader_readBool32(reader);
+ bg->backgroundDefinition = BinaryReader_readInt32(reader);
+ bg->x = BinaryReader_readInt32(reader);
+ bg->y = BinaryReader_readInt32(reader);
+ bg->tileX = BinaryReader_readInt32(reader);
+ bg->tileY = BinaryReader_readInt32(reader);
+ bg->speedX = BinaryReader_readInt32(reader);
+ bg->speedY = BinaryReader_readInt32(reader);
+ bg->stretch = BinaryReader_readBool32(reader);
+ }
+ for (uint32_t j = fillEnd; 8 > j; j++) {
+ memset(&room->backgrounds[j], 0, sizeof(RoomBackground));
+ }
+ free(bgPtrs);
+}
+
+static void readRoomViews(BinaryReader* reader, Room* room) {
+ uint32_t viewCount;
+ uint32_t* viewPtrsArr = readPointerTable(reader, &viewCount);
+ room->views = safeMalloc(8 * sizeof(RoomView));
+ for (uint32_t j = 0; viewCount > j && 8 > j; j++) {
+ BinaryReader_seek(reader, viewPtrsArr[j]);
+ RoomView* view = &room->views[j];
+ view->enabled = BinaryReader_readBool32(reader);
+ view->viewX = BinaryReader_readInt32(reader);
+ view->viewY = BinaryReader_readInt32(reader);
+ view->viewWidth = BinaryReader_readInt32(reader);
+ view->viewHeight = BinaryReader_readInt32(reader);
+ view->portX = BinaryReader_readInt32(reader);
+ view->portY = BinaryReader_readInt32(reader);
+ view->portWidth = BinaryReader_readInt32(reader);
+ view->portHeight = BinaryReader_readInt32(reader);
+ view->borderX = BinaryReader_readUint32(reader);
+ view->borderY = BinaryReader_readUint32(reader);
+ view->speedX = BinaryReader_readInt32(reader);
+ view->speedY = BinaryReader_readInt32(reader);
+ view->objectId = BinaryReader_readInt32(reader);
+ }
+ for (uint32_t j = viewCount; 8 > j; j++) {
+ memset(&room->views[j], 0, sizeof(RoomView));
+ }
+ free(viewPtrsArr);
+}
+
+static void readRoomGameObjects(BinaryReader* reader, DataWin* dw, Room* room) {
+ uint32_t objCount;
+ uint32_t* objPtrs = readPointerTable(reader, &objCount);
+ room->gameObjectCount = objCount;
+ if (objCount > 0) {
+ room->gameObjects = safeMalloc(objCount * sizeof(RoomGameObject));
+ repeat(objCount, j) {
+ BinaryReader_seek(reader, objPtrs[j]);
+ RoomGameObject* go = &room->gameObjects[j];
+ go->x = BinaryReader_readInt32(reader);
+ go->y = BinaryReader_readInt32(reader);
+ go->objectDefinition = BinaryReader_readInt32(reader);
+ go->instanceID = BinaryReader_readUint32(reader);
+ go->creationCode = BinaryReader_readInt32(reader);
+ go->scaleX = BinaryReader_readFloat32(reader);
+ go->scaleY = BinaryReader_readFloat32(reader);
+ if (DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) {
+ go->imageSpeed = BinaryReader_readFloat32(reader);
+ go->imageIndex = BinaryReader_readInt32(reader);
+ } else {
+ go->imageSpeed = 1.0f;
+ go->imageIndex = 0;
+ }
+ go->color = BinaryReader_readUint32(reader);
+ go->rotation = BinaryReader_readFloat32(reader);
+ if (dw->gen8.bytecodeVersion >= 16) {
+ go->preCreateCode = BinaryReader_readInt32(reader);
+ } else {
+ go->preCreateCode = -1;
+ }
+ }
+ } else {
+ room->gameObjects = nullptr;
+ }
+ free(objPtrs);
+}
+
+static void readRoomTiles(BinaryReader* reader, DataWin* dw, Room* room) {
+ uint32_t tileCount;
+ uint32_t* tilePtrs = readPointerTable(reader, &tileCount);
+ room->tileCount = tileCount;
+ if (tileCount > 0) {
+ room->tiles = safeMalloc(tileCount * sizeof(RoomTile));
+ repeat(tileCount, j) {
+ BinaryReader_seek(reader, tilePtrs[j]);
+ RoomTile* tile = &room->tiles[j];
+ tile->x = BinaryReader_readInt32(reader);
+ tile->y = BinaryReader_readInt32(reader);
+ tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0);
+ tile->backgroundDefinition = BinaryReader_readInt32(reader);
+ tile->sourceX = BinaryReader_readInt32(reader);
+ tile->sourceY = BinaryReader_readInt32(reader);
+ tile->width = BinaryReader_readUint32(reader);
+ tile->height = BinaryReader_readUint32(reader);
+ tile->tileDepth = BinaryReader_readInt32(reader);
+ tile->instanceID = BinaryReader_readUint32(reader);
+ tile->scaleX = BinaryReader_readFloat32(reader);
+ tile->scaleY = BinaryReader_readFloat32(reader);
+ tile->color = BinaryReader_readUint32(reader);
+ }
+ } else {
+ room->tiles = nullptr;
+ }
+ free(tilePtrs);
+}
+
+static void readRoomLayers(BinaryReader* reader, DataWin* dw, Room* room) {
+ uint32_t layerCount;
+ uint32_t* layerPtrs = readPointerTable(reader, &layerCount);
+ room->layerCount = layerCount;
+
+ if (layerCount == 0) {
+ room->layers = nullptr;
+ free(layerPtrs);
+ return;
+ }
+
+ room->layers = safeMalloc(layerCount * sizeof(RoomLayer));
+ repeat(layerCount, j) {
+ BinaryReader_seek(reader, layerPtrs[j]);
+ RoomLayer* layer = &room->layers[j];
+ layer->name = readStringPtr(reader, dw);
+ layer->id = BinaryReader_readUint32(reader);
+ layer->type = BinaryReader_readUint32(reader);
+ layer->depth = BinaryReader_readInt32(reader);
+ layer->xOffset = BinaryReader_readFloat32(reader);
+ layer->yOffset = BinaryReader_readFloat32(reader);
+ layer->hSpeed = BinaryReader_readFloat32(reader);
+ layer->vSpeed = BinaryReader_readFloat32(reader);
+ layer->visible = BinaryReader_readBool32(reader);
+ layer->assetsData = nullptr;
+ layer->backgroundData = nullptr;
+ layer->instancesData = nullptr;
+ layer->tilesData = nullptr;
+ if (DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) {
+ // EffectEnabled (bool32), EffectType (string ptr), EffectProperties (SimpleList)
+ BinaryReader_skip(reader, 4); // EffectEnabled
+ BinaryReader_skip(reader, 4); // EffectType (string ptr)
+ uint32_t effectPropCount = BinaryReader_readUint32(reader);
+ // Each EffectProperty is 12 bytes: Kind(int32) + Name(ptr) + Value(ptr)
+ BinaryReader_skip(reader, effectPropCount * 12);
+ }
+ switch (layer->type) {
+ case RoomLayerType_Path:
+ case RoomLayerType_Path2:
+ break; // Nothing to do
+ case RoomLayerType_Effect:
+ // In GMS 2022.1+, Effect layer data is empty (fields moved to layer header).
+ if (!DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) {
+ BinaryReader_skip(reader, 4); // EffectType (string ptr)
+ uint32_t propCount = BinaryReader_readUint32(reader);
+ BinaryReader_skip(reader, propCount * 12);
+ }
+ break;
+
+ case RoomLayerType_Assets: {
+ RoomLayerAssetsData* assets = safeMalloc(sizeof(RoomLayerAssetsData));
+ uint32_t legacyTilesPtr = BinaryReader_readUint32(reader);
+ uint32_t spritesPtr = BinaryReader_readUint32(reader);
+
+ BinaryReader_seek(reader, legacyTilesPtr);
+ uint32_t *innerTilePtrs = readPointerTable(reader, &assets->legacyTileCount);
+ if (assets->legacyTileCount > 0) {
+ assets->legacyTiles = safeMalloc(assets->legacyTileCount * sizeof(RoomTile));
+ repeat(assets->legacyTileCount, k) {
+ BinaryReader_seek(reader, innerTilePtrs[k]);
+ RoomTile* tile = &assets->legacyTiles[k];
+ tile->x = BinaryReader_readInt32(reader);
+ tile->y = BinaryReader_readInt32(reader);
+ tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0);
+ tile->backgroundDefinition = BinaryReader_readInt32(reader);
+ tile->sourceX = BinaryReader_readInt32(reader);
+ tile->sourceY = BinaryReader_readInt32(reader);
+ tile->width = BinaryReader_readUint32(reader);
+ tile->height = BinaryReader_readUint32(reader);
+ tile->tileDepth = BinaryReader_readInt32(reader);
+ tile->instanceID = BinaryReader_readUint32(reader);
+ tile->scaleX = BinaryReader_readFloat32(reader);
+ tile->scaleY = BinaryReader_readFloat32(reader);
+ tile->color = BinaryReader_readUint32(reader);
+ }
+ } else {
+ assets->legacyTiles = nullptr;
+ }
+ free(innerTilePtrs);
+
+ BinaryReader_seek(reader, spritesPtr);
+ uint32_t *spritePtrs = readPointerTable(reader, &assets->spriteCount);
+ if (assets->spriteCount > 0) {
+ assets->sprites = safeMalloc(assets->spriteCount * sizeof(SpriteInstance));
+ repeat(assets->spriteCount, k) {
+ BinaryReader_seek(reader, spritePtrs[k]);
+ SpriteInstance* sprite = &assets->sprites[k];
+ sprite->name = readStringPtr(reader, dw);
+ sprite->spriteIndex = BinaryReader_readInt32(reader);
+ sprite->x = BinaryReader_readInt32(reader);
+ sprite->y = BinaryReader_readInt32(reader);
+ sprite->scaleX = BinaryReader_readFloat32(reader);
+ sprite->scaleY = BinaryReader_readFloat32(reader);
+ sprite->color = BinaryReader_readUint32(reader);
+ sprite->animationSpeed = BinaryReader_readFloat32(reader);
+ sprite->animationSpeedType = BinaryReader_readUint32(reader);
+ sprite->frameIndex = BinaryReader_readFloat32(reader);
+ sprite->rotation = BinaryReader_readFloat32(reader);
+ }
+ } else {
+ assets->sprites = nullptr;
+ }
+ free(spritePtrs);
+
+ layer->assetsData = assets;
+ break;
+ }
+
+ case RoomLayerType_Background: {
+ RoomLayerBackgroundData* bg = safeMalloc(sizeof(RoomLayerBackgroundData));
+ bg->visible = BinaryReader_readBool32(reader);
+ bg->foreground = BinaryReader_readBool32(reader);
+ bg->spriteIndex = BinaryReader_readInt32(reader);
+ bg->hTiled = BinaryReader_readBool32(reader);
+ bg->vTiled = BinaryReader_readBool32(reader);
+ bg->stretch = BinaryReader_readBool32(reader);
+ bg->color = BinaryReader_readUint32(reader);
+ bg->firstFrame = BinaryReader_readFloat32(reader);
+ bg->animSpeed = BinaryReader_readFloat32(reader);
+ bg->animSpeedType = BinaryReader_readUint32(reader);
+ layer->backgroundData = bg;
+ break;
+ }
+ case RoomLayerType_Instances: {
+ RoomLayerInstancesData* inst = safeMalloc(sizeof(RoomLayerInstancesData));
+ inst->instanceCount = BinaryReader_readUint32(reader);
+ if (inst->instanceCount > 0) {
+ inst->instanceIds = safeMalloc(inst->instanceCount * sizeof(uint32_t));
+ repeat(inst->instanceCount, k) {
+ inst->instanceIds[k] = BinaryReader_readUint32(reader);
+ }
+ } else {
+ inst->instanceIds = nullptr;
+ }
+ layer->instancesData = inst;
+ break;
+ }
+ case RoomLayerType_Tiles: {
+ RoomLayerTilesData* tiles = safeMalloc(sizeof(RoomLayerTilesData));
+ tiles->backgroundIndex = BinaryReader_readInt32(reader);
+ tiles->tilesX = BinaryReader_readUint32(reader);
+ tiles->tilesY = BinaryReader_readUint32(reader);
+ uint32_t totalTiles = tiles->tilesX * tiles->tilesY;
+ if (totalTiles > 0) {
+ tiles->tileData = safeMalloc(totalTiles * sizeof(uint32_t));
+ repeat(totalTiles, k) {
+ tiles->tileData[k] = BinaryReader_readUint32(reader);
+ }
+ } else {
+ tiles->tileData = nullptr;
+ }
+ layer->tilesData = tiles;
+ break;
+ }
+ default: {
+ fprintf(stderr, "Unsupported Room Layer Type %u\n", layer->type);
+ exit(0);
+ }
+ }
+ }
+ free(layerPtrs);
+}
+
+// Reads all 5 payload sections for a single room via the given reader.
+// Assumes the caller has populated room->*FileOffset from the header pass.
+static void readRoomPayload(BinaryReader* reader, DataWin* dw, Room* room) {
+ require(!room->payloadLoaded);
+
+ BinaryReader_seek(reader, room->backgroundsFileOffset);
+ readRoomBackgrounds(reader, room);
+
+ BinaryReader_seek(reader, room->viewsFileOffset);
+ readRoomViews(reader, room);
+
+ BinaryReader_seek(reader, room->gameObjectsFileOffset);
+ readRoomGameObjects(reader, dw, room);
+
+ BinaryReader_seek(reader, room->tilesFileOffset);
+ readRoomTiles(reader, dw, room);
+
+ room->layerCount = 0;
+ room->layers = nullptr;
+ if (room->layersFileOffset != 0) {
+ BinaryReader_seek(reader, room->layersFileOffset);
+ readRoomLayers(reader, dw, room);
+ }
+
+ room->payloadLoaded = true;
+}
+
+// Returns true when "name" is in the eager-load set.
+static bool isRoomNameInEagerList(const char* name, StringBooleanEntry* eagerSet) {
+ if (name == nullptr || eagerSet == nullptr) return false;
+ return shgeti(eagerSet, name) >= 0;
+}
+
+static void parseROOM(BinaryReader* reader, DataWin* dw, bool lazyLoadRooms, StringBooleanEntry* eagerlyLoadedRooms) {
+ RoomChunk* rc = &dw->room;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ rc->count = count;
+
+ if (count == 0) { free(ptrs); rc->rooms = nullptr; return; }
+
+ // Detect whether RoomGameObject includes ImageSpeed/ImageIndex fields (added in GMS 2.2.2.302).
+ // UndertaleModTool detects this via the distance between the first two game object pointers: 40 bytes = legacy format, 48 bytes = new format with ImageSpeed+ImageIndex.
+ // We skip if we already know that we are at or above 2.2.2.302.
+ if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0) && !DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) {
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ // Room header layout (before gameObjectsPtr): name, caption, width, height, speed, persistent,
+ // bgColor, drawBgColor, creationCodeId, flags, backgroundsPtr, viewsPtr = 12 uint32s.
+ BinaryReader_skip(reader, 12 * 4);
+ uint32_t gameObjectsPtr = BinaryReader_readUint32(reader);
+ BinaryReader_seek(reader, gameObjectsPtr);
+ uint32_t objCount = BinaryReader_readUint32(reader);
+ if (objCount >= 2) {
+ uint32_t firstPtr = BinaryReader_readUint32(reader);
+ uint32_t secondPtr = BinaryReader_readUint32(reader);
+ if (secondPtr - firstPtr == 48) {
+ DataWin_bumpVersionTo(dw, 2, 2, 2, 302);
+ }
+ break;
+ }
+ }
+ }
+
+ // Detect whether Layer headers include EffectEnabled/EffectType/EffectProperties fields (added in GMS 2022.1).
+ if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) {
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ // Room header before layersPtr: 22 uint32s (name..metersPerPixel).
+ BinaryReader_skip(reader, 22 * 4);
+ uint32_t layersPtr = BinaryReader_readUint32(reader);
+ uint32_t seqnPtr = BinaryReader_readUint32(reader);
+ BinaryReader_seek(reader, layersPtr);
+ uint32_t layerCount = BinaryReader_readUint32(reader);
+ if (layerCount == 0) continue;
+ uint32_t jumpOffset = BinaryReader_readUint32(reader);
+ uint32_t nextOffset = (layerCount == 1) ? seqnPtr : BinaryReader_readUint32(reader);
+ // Layer header: name(4) id(4) type(4) depth(4) xOff(4) yOff(4) hSpd(4) vSpd(4) visible(4) = 9 uint32s = 36 bytes.
+ // jumpOffset points to start of the layer; we seek to jumpOffset+8 to skip name+id then read type.
+ BinaryReader_seek(reader, jumpOffset + 8);
+ uint32_t layerType = BinaryReader_readUint32(reader);
+ if (layerType == RoomLayerType_Path || layerType == RoomLayerType_Path2) continue;
+ bool detected = false;
+ switch (layerType) {
+ case RoomLayerType_Background: {
+ // After type, there's depth+xOff+yOff+hSpd+vSpd+visible = 6*4 = 24, then 10 background fields = 40 bytes.
+ // Total legacy body after type read: 24 + 40 = 64 bytes. 2022.1 adds effect data > 64 bytes of additional data past the next layer boundary.
+ size_t absPos = BinaryReader_getPosition(reader);
+ if (nextOffset - absPos > 16 * 4) detected = true;
+ break;
+ }
+ case RoomLayerType_Instances: {
+ BinaryReader_skip(reader, 6 * 4);
+ uint32_t instanceCount = BinaryReader_readUint32(reader);
+ size_t absPos = BinaryReader_getPosition(reader);
+ if (nextOffset - absPos != instanceCount * 4) detected = true;
+ break;
+ }
+ case RoomLayerType_Assets: {
+ BinaryReader_skip(reader, 6 * 4);
+ uint32_t tileOffset = BinaryReader_readUint32(reader);
+ size_t absPos = BinaryReader_getPosition(reader);
+ if (tileOffset != absPos + 8 && tileOffset != absPos + 12) detected = true;
+ break;
+ }
+ case RoomLayerType_Tiles: {
+ BinaryReader_skip(reader, 7 * 4);
+ uint32_t tileMapWidth = BinaryReader_readUint32(reader);
+ uint32_t tileMapHeight = BinaryReader_readUint32(reader);
+ size_t absPos = BinaryReader_getPosition(reader);
+ if (nextOffset - absPos != tileMapWidth * tileMapHeight * 4) detected = true;
+ break;
+ }
+ case RoomLayerType_Effect: {
+ BinaryReader_skip(reader, 7 * 4);
+ uint32_t propertyCount = BinaryReader_readUint32(reader);
+ size_t absPos = BinaryReader_getPosition(reader);
+ if (nextOffset - absPos != propertyCount * 3 * 4) detected = true;
+ break;
+ }
+ }
+ if (detected) DataWin_bumpVersionTo(dw, 2022, 1, 0, 0);
+ break;
+ }
+ }
+
+ rc->rooms = safeCalloc(count, sizeof(Room));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ Room* room = &rc->rooms[i];
+
+ // ===[ Header pass ]===
+ room->name = readStringPtr(reader, dw);
+ room->caption = readStringPtr(reader, dw);
+ room->width = BinaryReader_readUint32(reader);
+ room->height = BinaryReader_readUint32(reader);
+ room->speed = BinaryReader_readUint32(reader);
+ room->persistent = BinaryReader_readBool32(reader);
+ room->backgroundColor = BinaryReader_readUint32(reader);
+ room->drawBackgroundColor = BinaryReader_readBool32(reader);
+ room->creationCodeId = BinaryReader_readInt32(reader);
+ room->flags = BinaryReader_readUint32(reader);
+ room->backgroundsFileOffset = BinaryReader_readUint32(reader);
+ room->viewsFileOffset = BinaryReader_readUint32(reader);
+ room->gameObjectsFileOffset = BinaryReader_readUint32(reader);
+ room->tilesFileOffset = BinaryReader_readUint32(reader);
+ room->world = BinaryReader_readBool32(reader);
+ room->top = BinaryReader_readUint32(reader);
+ room->left = BinaryReader_readUint32(reader);
+ room->right = BinaryReader_readUint32(reader);
+ room->bottom = BinaryReader_readUint32(reader);
+ room->gravityX = BinaryReader_readFloat32(reader);
+ room->gravityY = BinaryReader_readFloat32(reader);
+ room->metersPerPixel = BinaryReader_readFloat32(reader);
+ if (DataWin_isVersionAtLeast(dw, 2024, 13, 0, 0)) {
+ // skip instanceCreationOrderIDs
+ int icCount = BinaryReader_readInt32(reader);
+ BinaryReader_skip(reader, sizeof(int32_t) * icCount);
+ }
+ room->layersFileOffset = 0;
+ if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) {
+ room->layersFileOffset = BinaryReader_readUint32(reader);
+ if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0)) {
+ BinaryReader_skip(reader, 4); // sequencesPtr
+ }
+ }
+
+ room->payloadLoaded = false;
+ room->eagerlyLoaded = false;
+ room->backgrounds = nullptr;
+ room->views = nullptr;
+ room->gameObjects = nullptr;
+ room->gameObjectCount = 0;
+ room->tiles = nullptr;
+ room->tileCount = 0;
+ room->layers = nullptr;
+ room->layerCount = 0;
+
+ // Load the room payload if needed
+ bool eager = !lazyLoadRooms || isRoomNameInEagerList(room->name, eagerlyLoadedRooms);
+ if (eager) {
+ readRoomPayload(reader, dw, room);
+ if (lazyLoadRooms) {
+ room->eagerlyLoaded = true;
+ }
+ }
+ }
+ free(ptrs);
+}
+
+// Sprite/Background/Font initially store an absolute file offset to their TexturePageItem (since SPRT/BGND/FONT are parsed before TPAG).
+// resolveAllTPAGReferences translates those offsets to TPAG indices once the table is known. ptrs[] is the TPAG pointer table in monotonically increasing file order, so we can binary search it.
+// Offsets that don't resolve (or are 0) become -1.
+static int32_t findTPAGIndexByOffset(uint32_t* ptrs, uint32_t count, uint32_t offset) {
+ if (offset == 0) return -1;
+ uint32_t lo = 0, hi = count;
+ while (hi > lo) {
+ uint32_t mid = (lo + hi) >> 1;
+ uint32_t v = ptrs[mid];
+ if (v == offset) return (int32_t) mid;
+ if (offset > v) lo = mid + 1; else hi = mid;
+ }
+ return -1;
+}
+
+static void resolveAllTPAGReferences(DataWin* dw, uint32_t* ptrs, uint32_t count) {
+ repeat(dw->sprt.count, i) {
+ Sprite* spr = &dw->sprt.sprites[i];
+ repeat(spr->textureCount, j) {
+ spr->tpagIndices[j] = findTPAGIndexByOffset(ptrs, count, (uint32_t) spr->tpagIndices[j]);
+ }
+ }
+ repeat(dw->bgnd.count, i) {
+ Background* bg = &dw->bgnd.backgrounds[i];
+ bg->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) bg->tpagIndex);
+ }
+ repeat(dw->font.count, i) {
+ Font* fnt = &dw->font.fonts[i];
+ fnt->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) fnt->tpagIndex);
+ }
+}
+
+static void parseTPAG(BinaryReader* reader, DataWin* dw) {
+ Tpag* t = &dw->tpag;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ t->count = count;
+
+ if (count == 0) { free(ptrs); t->items = nullptr; return; }
+
+ t->items = safeMalloc(count * sizeof(TexturePageItem));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ TexturePageItem* item = &t->items[i];
+ item->sourceX = BinaryReader_readUint16(reader);
+ item->sourceY = BinaryReader_readUint16(reader);
+ item->sourceWidth = BinaryReader_readUint16(reader);
+ item->sourceHeight = BinaryReader_readUint16(reader);
+ item->targetX = BinaryReader_readUint16(reader);
+ item->targetY = BinaryReader_readUint16(reader);
+ item->targetWidth = BinaryReader_readUint16(reader);
+ item->targetHeight = BinaryReader_readUint16(reader);
+ item->boundingWidth = BinaryReader_readUint16(reader);
+ item->boundingHeight = BinaryReader_readUint16(reader);
+ item->texturePageId = BinaryReader_readInt16(reader);
+ }
+
+ resolveAllTPAGReferences(dw, ptrs, count);
+
+ free(ptrs);
+}
+
+static void parseCODE(BinaryReader* reader, DataWin* dw, uint32_t chunkLength, size_t chunkDataStart) {
+ Code* c = &dw->code;
+
+ if (chunkLength == 0) {
+ // YYC-compiled game, no bytecode
+ c->count = 0;
+ c->entries = nullptr;
+ return;
+ }
+
+ // Standard pointer list at chunk start. Each entry has a relative offset
+ // (bytecodeRelAddr) that points to the actual bytecode blob elsewhere in the chunk.
+
+ uint32_t codeCount;
+ uint32_t* codePtrs = readPointerTable(reader, &codeCount);
+ c->count = codeCount;
+
+ if (codeCount == 0) { free(codePtrs); c->entries = nullptr; return; }
+
+ c->entries = safeMalloc(codeCount * sizeof(CodeEntry));
+ repeat(codeCount, i) {
+ BinaryReader_seek(reader, codePtrs[i]);
+ CodeEntry* entry = &c->entries[i];
+ entry->name = readStringPtr(reader, dw);
+ entry->length = BinaryReader_readUint32(reader);
+ entry->localsCount = BinaryReader_readUint16(reader);
+ entry->argumentsCount = BinaryReader_readUint16(reader);
+
+ // bytecodeRelAddr is relative to the position of this field
+ size_t relAddrFieldPos = BinaryReader_getPosition(reader);
+ int32_t bytecodeRelAddr = BinaryReader_readInt32(reader);
+ entry->bytecodeAbsoluteOffset = (uint32_t)((int64_t)relAddrFieldPos + bytecodeRelAddr);
+
+ entry->offset = BinaryReader_readUint32(reader);
+ }
+ free(codePtrs);
+
+ // Compute bytecode blob range and load into owned buffer.
+ // The bytecode blob starts at the minimum bytecodeAbsoluteOffset and
+ // extends to the end of the CODE chunk.
+ uint32_t blobStart = c->entries[0].bytecodeAbsoluteOffset;
+ repeat(codeCount, i) {
+ if (c->entries[i].bytecodeAbsoluteOffset < blobStart) {
+ blobStart = c->entries[i].bytecodeAbsoluteOffset;
+ }
+ }
+ size_t chunkEnd = chunkDataStart + chunkLength;
+ size_t blobSize = chunkEnd - blobStart;
+
+ dw->bytecodeBufferBase = blobStart;
+ dw->bytecodeBuffer = BinaryReader_readBytesAt(reader, blobStart, blobSize);
+}
+
+static void parseVARI(BinaryReader* reader, DataWin* dw, uint32_t chunkLength) {
+ Vari* v = &dw->vari;
+
+ v->varCount1 = BinaryReader_readUint32(reader);
+ v->varCount2 = BinaryReader_readUint32(reader);
+ v->maxLocalVarCount = BinaryReader_readUint32(reader);
+
+ // Variable entries are packed sequentially (no pointer table)
+ // Number of entries = (chunkLength - 12) / 20
+ v->variableCount = (chunkLength - 12) / 20;
+
+ if (v->variableCount > 0) {
+ v->variables = safeMalloc(v->variableCount * sizeof(Variable));
+ repeat(v->variableCount, i) {
+ Variable* var = &v->variables[i];
+ var->name = readStringPtr(reader, dw);
+ var->instanceType = BinaryReader_readInt32(reader);
+ var->varID = BinaryReader_readInt32(reader);
+ var->occurrences = BinaryReader_readUint32(reader);
+ var->firstAddress = BinaryReader_readUint32(reader);
+ }
+ } else {
+ v->variables = nullptr;
+ }
+}
+
+static void parseFUNC(BinaryReader* reader, DataWin* dw) {
+ Func* f = &dw->func;
+
+ // Part 1: Functions SimpleList
+ f->functionCount = BinaryReader_readUint32(reader);
+ if (f->functionCount > 0) {
+ f->functions = safeMalloc(f->functionCount * sizeof(Function));
+ repeat(f->functionCount, i) {
+ f->functions[i].name = readStringPtr(reader, dw);
+ f->functions[i].occurrences = BinaryReader_readUint32(reader);
+ uint32_t rawAddr = BinaryReader_readUint32(reader);
+ // In GMS 2.3+, firstAddress points to the operand word (instruction + 4), not the instruction itself
+ if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && rawAddr != (uint32_t) -1) {
+ rawAddr -= 4;
+ }
+ f->functions[i].firstAddress = rawAddr;
+ }
+ } else {
+ f->functions = nullptr;
+ }
+
+ // Part 2: Code Locals SimpleList
+ f->codeLocalsCount = BinaryReader_readUint32(reader);
+ if (f->codeLocalsCount > 0) {
+ f->codeLocals = safeMalloc(f->codeLocalsCount * sizeof(CodeLocals));
+ repeat(f->codeLocalsCount, i) {
+ CodeLocals* cl = &f->codeLocals[i];
+ cl->localVarCount = BinaryReader_readUint32(reader);
+ cl->name = readStringPtr(reader, dw);
+
+ if (cl->localVarCount > 0) {
+ cl->locals = safeMalloc(cl->localVarCount * sizeof(LocalVar));
+ repeat(cl->localVarCount, j) {
+ cl->locals[j].varID = BinaryReader_readUint32(reader);
+ cl->locals[j].name = readStringPtr(reader, dw);
+ }
+ } else {
+ cl->locals = nullptr;
+ }
+ }
+ } else {
+ f->codeLocals = nullptr;
+ }
+}
+
+static void parseSTRG(BinaryReader* reader, DataWin* dw) {
+ Strg* s = &dw->strg;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ s->count = count;
+
+ if (count == 0) { free(ptrs); s->strings = nullptr; return; }
+
+ s->strings = safeMalloc(count * sizeof(const char*));
+ repeat(count, i) {
+ // Pointer table points to the string's length prefix.
+ // The actual string content starts 4 bytes after.
+ s->strings[i] = (const char*)(dw->strgBuffer + (ptrs[i] + 4 - dw->strgBufferBase));
+ }
+ free(ptrs);
+}
+
+static void parseTXTR(BinaryReader* reader, DataWin* dw, size_t chunkEnd) {
+ Txtr* t = &dw->txtr;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ t->count = count;
+
+ if (count == 0) { free(ptrs); t->textures = nullptr; return; }
+
+ // Read metadata entries
+ bool hasGeneratedMips = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0);
+
+ // Detect GMS 2022.3+ (TextureBlockSize field) and 2022.9+ (Width/Height/IndexInGroup fields) by probing the distance between the first two entry pointers.
+ // Only works when there are at least 2 textures (which is almost always the case for real games).
+ // Layouts:
+ // pre-2022.3: scaled+generatedMips+blobOffset = 12 bytes
+ // 2022.3+: ... + textureBlockSize = 16 bytes
+ // 2022.9+: ... + width + height + indexInGroup = 28 bytes
+ bool has2022_3 = DataWin_isVersionAtLeast(dw, 2022, 3, 0, 0);
+ bool has2022_9 = DataWin_isVersionAtLeast(dw, 2022, 9, 0, 0);
+ if (count >= 2 && hasGeneratedMips && !has2022_9) {
+ uint32_t diff = ptrs[1] - ptrs[0];
+ if (diff == 28) {
+ DataWin_bumpVersionTo(dw, 2022, 9, 0, 0);
+ has2022_3 = true;
+ has2022_9 = true;
+ } else if (diff == 16 && !has2022_3) {
+ DataWin_bumpVersionTo(dw, 2022, 3, 0, 0);
+ has2022_3 = true;
+ }
+ }
+
+ t->textures = safeMalloc(count * sizeof(Texture));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ t->textures[i].scaled = BinaryReader_readUint32(reader);
+ if (hasGeneratedMips) {
+ t->textures[i].generatedMips = BinaryReader_readUint32(reader);
+ } else {
+ t->textures[i].generatedMips = 0;
+ }
+ if (has2022_3) {
+ t->textures[i].textureBlockSize = BinaryReader_readUint32(reader);
+ } else {
+ t->textures[i].textureBlockSize = 0;
+ }
+ if (has2022_9) {
+ t->textures[i].textureWidth = BinaryReader_readInt32(reader);
+ t->textures[i].textureHeight = BinaryReader_readInt32(reader);
+ t->textures[i].indexInGroup = BinaryReader_readInt32(reader);
+ } else {
+ t->textures[i].textureWidth = 0;
+ t->textures[i].textureHeight = 0;
+ t->textures[i].indexInGroup = 0;
+ }
+ t->textures[i].blobOffset = BinaryReader_readUint32(reader);
+ t->textures[i].blobData = nullptr;
+ }
+ free(ptrs);
+
+ // Compute blob sizes from successive offsets
+ repeat(count, i) {
+ if (t->textures[i].blobOffset == 0) {
+ t->textures[i].blobSize = 0; // external texture
+ continue;
+ }
+ if (count > i + 1 && t->textures[i + 1].blobOffset != 0) {
+ t->textures[i].blobSize = t->textures[i + 1].blobOffset - t->textures[i].blobOffset;
+ } else {
+ t->textures[i].blobSize = (uint32_t)(chunkEnd - t->textures[i].blobOffset);
+ }
+ }
+
+ // Load blob data into owned buffers
+ repeat(count, i) {
+ if (t->textures[i].blobOffset == 0 || t->textures[i].blobSize == 0) continue;
+ t->textures[i].blobData = BinaryReader_readBytesAt(reader, t->textures[i].blobOffset, t->textures[i].blobSize);
+ }
+}
+
+static void parseAUDO(BinaryReader* reader, DataWin* dw, bool headersOnly) {
+ Audo* a = &dw->audo;
+
+ uint32_t count;
+ uint32_t* ptrs = readPointerTable(reader, &count);
+ a->count = count;
+
+ if (count == 0) { free(ptrs); a->entries = nullptr; return; }
+
+ a->entries = safeMalloc(count * sizeof(AudioEntry));
+ repeat(count, i) {
+ BinaryReader_seek(reader, ptrs[i]);
+ a->entries[i].dataSize = BinaryReader_readUint32(reader);
+ a->entries[i].dataOffset = (uint32_t)BinaryReader_getPosition(reader);
+ // Load audio data into owned buffer
+ if (headersOnly) {
+ a->entries[i].data = nullptr;
+ } else if (a->entries[i].dataSize > 0) {
+ a->entries[i].data = safeMalloc(a->entries[i].dataSize);
+ BinaryReader_readBytes(reader, a->entries[i].data, a->entries[i].dataSize);
+ } else {
+ a->entries[i].data = nullptr;
+ }
+ }
+ free(ptrs);
+}
+
+// ===[ MAIN PARSE FUNCTION ]===
+
+DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) {
+ FILE* file = fopen(filePath, "rb");
+ if (!file) {
+ fprintf(stderr, "Failed to open file: %s\n", filePath);
+ exit(1);
+ }
+
+ // Use a large read buffer to reduce the number of physical reads
+ // This is critical for slow I/O devices like the PS2 CDVD drive, where each fread
+ // call would otherwise trigger a separate disc read of just a few sectors
+ setvbuf(file, nullptr, _IOFBF, 128 * 1024);
+
+ fseek(file, 0, SEEK_END);
+ long fileSize = ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ if (fileSize <= 0) {
+ fprintf(stderr, "Invalid file size: %ld\n", fileSize);
+ fclose(file);
+ exit(1);
+ }
+
+ // Allocate and zero-initialize DataWin
+ DataWin* dw = safeCalloc(1, sizeof(DataWin));
+
+ BinaryReader reader = BinaryReader_create(file, (size_t) fileSize);
+
+ // Validate FORM header
+ char formMagic[4];
+ BinaryReader_readBytes(&reader, formMagic, 4);
+ if (memcmp(formMagic, "FORM", 4) != 0) {
+ fprintf(stderr, "Invalid file: expected FORM magic, got '%.4s'\n", formMagic);
+ free(dw);
+ fclose(file);
+ exit(1);
+ }
+
+ uint32_t formLength = BinaryReader_readUint32(&reader);
+ (void) formLength;
+
+ // Pass 1: Count total chunks and find STRG chunk offset.
+ // All other chunks reference strings from STRG, so it must be loaded first.
+ // We also check if the CODE chunk exists.
+ int totalChunks = 0;
+ bool codeExists = false;
+ BinaryReader_seek(&reader, 8); // reset to after FORM header
+
+ while ((size_t) fileSize > BinaryReader_getPosition(&reader)) {
+ if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break;
+
+ char chunkName[5] = {0};
+ BinaryReader_readBytes(&reader, chunkName, 4);
+ uint32_t chunkLength = BinaryReader_readUint32(&reader);
+ size_t chunkDataStart = BinaryReader_getPosition(&reader);
+
+ if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) {
+ dw->strgBufferBase = chunkDataStart;
+ dw->strgBuffer = BinaryReader_readBytesAt(&reader, chunkDataStart, chunkLength);
+ }
+
+ if ((memcmp(chunkName, "CODE", 4) == 0) && chunkLength > 0) {
+ codeExists = true;
+ }
+
+ // Bump detected version based on chunk presence, so later chunks can use the right version during parsing (parseOBJT needs to know we're >= 2.3 to probe for the GMS 2022.5+ Managed field).
+ if (memcmp(chunkName, "ACRV", 4) == 0 || memcmp(chunkName, "SEQN", 4) == 0 || memcmp(chunkName, "TAGS", 4) == 0) {
+ DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
+ } else if (memcmp(chunkName, "FEDS", 4) == 0) {
+ DataWin_bumpVersionTo(dw, 2, 3, 6, 0);
+ } else if (memcmp(chunkName, "FEAT", 4) == 0) {
+ DataWin_bumpVersionTo(dw, 2022, 8, 0, 0);
+ } else if (memcmp(chunkName, "UILR", 4) == 0) {
+ DataWin_bumpVersionTo(dw, 2024, 13, 0, 0);
+ } else if (memcmp(chunkName, "PSEM", 4) == 0 || memcmp(chunkName, "PSYS", 4) == 0) {
+ DataWin_bumpVersionTo(dw, 2023, 2, 0, 0);
+ }
+
+ BinaryReader_seek(&reader, chunkDataStart + chunkLength);
+ totalChunks++;
+ }
+
+ if (!codeExists && options.parseCode) {
+ fprintf(stderr, "CODE chunk does not exist or is empty! This usually means you're loading a YYC game.\n");
+ fclose(file);
+ exit(1);
+ }
+
+ // Pass 2: Parse all chunks
+ // For each chunk that will be parsed, we bulk-read the entire chunk into memory first
+ // and then parse from the memory buffer. This dramatically reduces the number of physical
+ // reads on slow I/O devices like the PS2 CDVD drive.
+ BinaryReader_seek(&reader, 8); // skip past FORM header
+ int chunkIndex = 0;
+ while ((size_t) fileSize > BinaryReader_getPosition(&reader)) {
+ if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break;
+
+ char chunkName[5] = {0};
+ BinaryReader_readBytes(&reader, chunkName, 4);
+ uint32_t chunkLength = BinaryReader_readUint32(&reader);
+ size_t chunkDataStart = BinaryReader_getPosition(&reader);
+ size_t chunkEnd = chunkDataStart + chunkLength;
+
+ if (options.progressCallback) {
+ options.progressCallback(chunkName, chunkIndex, totalChunks, dw, options.progressCallbackUserData);
+ }
+
+ // Determine if this chunk will be parsed (and thus needs bulk loading)
+ bool shouldParse =
+ (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) ||
+ (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) ||
+ (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) ||
+ (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) ||
+ (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) ||
+ (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) ||
+ (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) ||
+ (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) ||
+ (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) ||
+ (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) ||
+ (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) ||
+ (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) ||
+ (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) ||
+ (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) ||
+ (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) ||
+ (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) ||
+ (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) ||
+ (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) ||
+ (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) ||
+ (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) ||
+ (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) ||
+ (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) ||
+ (options.parseAudo && memcmp(chunkName, "AUDO", 4) == 0) ||
+ (options.parseAudoHeadersOnly && memcmp(chunkName, "AUDO", 4) == 0);
+
+ // Bulk-read the chunk data into memory for fast parsing.
+ bool audoHeadersOnly = options.parseAudoHeadersOnly && memcmp(chunkName, "AUDO", 4) == 0;
+ uint8_t* chunkBuffer = nullptr;
+ if (shouldParse && chunkLength > 0 && !audoHeadersOnly) {
+ chunkBuffer = safeMalloc(chunkLength);
+ size_t read = fread(chunkBuffer, 1, chunkLength, reader.file);
+ if (read != chunkLength) {
+ fprintf(stderr, "DataWin: short read on chunk %.4s (expected %u, got %zu)\n", chunkName, chunkLength, read);
+ exit(1);
+ }
+ BinaryReader_setBuffer(&reader, chunkBuffer, chunkDataStart, chunkLength);
+ }
+
+ if (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) {
+ parseGEN8(&reader, dw);
+ } else if (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) {
+ parseOPTN(&reader, dw);
+ } else if (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) {
+ parseLANG(&reader, dw);
+ } else if (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) {
+ parseEXTN(&reader, dw);
+ } else if (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) {
+ parseSOND(&reader, dw);
+ } else if (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) {
+ parseAGRP(&reader, dw);
+ } else if (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) {
+ parseSPRT(&reader, dw, options.skipLoadingPreciseMasksForNonPreciseSprites);
+ } else if (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) {
+ parseBGND(&reader, dw);
+ } else if (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) {
+ parsePATH(&reader, dw);
+ } else if (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) {
+ parseSCPT(&reader, dw);
+ } else if (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) {
+ parseGLOB(&reader, dw);
+ } else if (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) {
+ parseSHDR(&reader, dw);
+ } else if (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) {
+ parseFONT(&reader, dw);
+ } else if (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) {
+ parseTMLN(&reader, dw);
+ } else if (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) {
+ parseOBJT(&reader, dw);
+ } else if (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) {
+ parseROOM(&reader, dw, options.lazyLoadRooms, options.eagerlyLoadedRooms);
+ } else if (memcmp(chunkName, "DAFL", 4) == 0) {
+ // Empty chunk, nothing to parse
+ } else if (memcmp(chunkName, "EMBI", 4) == 0) {
+ // Embedded Images chunk
+ } else if (memcmp(chunkName, "TGIN", 4) == 0) {
+ // Texture Group Info chunk (bytecodeVersion >= 17)
+ } else if (memcmp(chunkName, "ACRV", 4) == 0) {
+ // Animation Curves chunk (GMS 2.3+)
+ DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
+ } else if (memcmp(chunkName, "SEQN", 4) == 0) {
+ // Sequences chunk (GMS 2.3+)
+ DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
+ } else if (memcmp(chunkName, "TAGS", 4) == 0) {
+ // Tags chunk (GMS 2.3+)
+ DataWin_bumpVersionTo(dw, 2, 3, 0, 0);
+ } else if (memcmp(chunkName, "FEDS", 4) == 0) {
+ // Filter Effects Data chunk (GMS 2.3.6+)
+ DataWin_bumpVersionTo(dw, 2, 3, 6, 0);
+ } else if (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) {
+ parseTPAG(&reader, dw);
+ } else if (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) {
+ parseCODE(&reader, dw, chunkLength, chunkDataStart);
+ } else if (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) {
+ parseVARI(&reader, dw, chunkLength);
+ } else if (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) {
+ parseFUNC(&reader, dw);
+ } else if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) {
+ parseSTRG(&reader, dw);
+ } else if (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) {
+ parseTXTR(&reader, dw, chunkEnd);
+ } else if ((options.parseAudo || options.parseAudoHeadersOnly) && memcmp(chunkName, "AUDO", 4) == 0) {
+ parseAUDO(&reader, dw, options.parseAudoHeadersOnly);
+ } else {
+ printf("Unknown chunk: %.4s (length %u at offset 0x%zX)\n", chunkName, chunkLength, chunkDataStart - 8);
+ }
+
+ // Free the chunk buffer and revert to FILE*-based reads for the next header
+ if (chunkBuffer != nullptr) {
+ BinaryReader_clearBuffer(&reader);
+ free(chunkBuffer);
+ }
+
+ // Seek to chunk end (skip any unread data or trailing padding)
+ fseek(reader.file, (long) chunkEnd, SEEK_SET);
+ chunkIndex++;
+ }
+
+ // GMS2: apply default FPS to rooms with speed=0
+ if (dw->gen8.gms2FPS > 0) {
+ repeat(dw->room.count, i) {
+ if (dw->room.rooms[i].speed == 0) {
+ dw->room.rooms[i].speed = (uint32_t) dw->gen8.gms2FPS;
+ }
+ }
+ }
+
+ // If lazy-loading rooms, keep the file handle open for DataWin_loadRoomPayload, otherwise close it now
+ dw->lazyLoadRooms = options.lazyLoadRooms;
+ if (options.lazyLoadRooms) {
+ dw->lazyLoadFile = file;
+ dw->lazyLoadFilePath = safeStrdup(filePath);
+ dw->fileSize = (size_t) fileSize;
+ } else {
+ dw->lazyLoadFile = nullptr;
+ dw->lazyLoadFilePath = nullptr;
+ dw->fileSize = 0;
+ if (!options.parseAudoHeadersOnly) {
+ fclose(file);
+ }
+ }
+
+ // If headers-only AUDO was used, open a dedicated file handle for on-demand audio reads.
+ // This is separate from lazyLoadFile so room-load and audio-load don't race on one FILE*.
+ if (options.parseAudoHeadersOnly) {
+ dw->lazyAudioFile = fopen(filePath, "rb");
+ if (dw->lazyAudioFile == NULL) {
+ fprintf(stderr, "DataWin: failed to reopen %s for lazy audio reads\n", filePath);
+ }
+ if (!options.lazyLoadRooms) {
+ fclose(file); // close the parse-time handle now that we've reopened for audio
+ }
+ } else {
+ dw->lazyAudioFile = nullptr;
+ }
+
+ return dw;
+}
+
+// ===[ FREE ]===
+
+void DataWin_free(DataWin* dw) {
+ if (!dw) return;
+
+ // GEN8
+ free(dw->gen8.roomOrder);
+
+ // OPTN
+ free(dw->optn.constants);
+
+ // LANG
+ free(dw->lang.entryIds);
+ if (dw->lang.languages) {
+ repeat(dw->lang.languageCount, i) {
+ free(dw->lang.languages[i].entries);
+ }
+ free(dw->lang.languages);
+ }
+
+ // EXTN
+ if (dw->extn.extensions) {
+ repeat(dw->extn.count, i) {
+ Extension* ext = &dw->extn.extensions[i];
+ if (ext->files) {
+ repeat(ext->fileCount, j) {
+ ExtensionFile* file = &ext->files[j];
+ if (file->functions) {
+ repeat(file->functionCount, k) {
+ free(file->functions[k].arguments);
+ }
+ free(file->functions);
+ }
+ }
+ free(ext->files);
+ }
+ }
+ free(dw->extn.extensions);
+ }
+
+ // SOND
+ free(dw->sond.sounds);
+
+ // AGRP
+ free(dw->agrp.audioGroups);
+
+ // SPRT
+ if (dw->sprt.sprites) {
+ repeat(dw->sprt.count, i) {
+ free(dw->sprt.sprites[i].tpagIndices);
+ if (dw->sprt.sprites[i].masks != nullptr) {
+ repeat(dw->sprt.sprites[i].maskCount, j) {
+ free(dw->sprt.sprites[i].masks[j]);
+ }
+ free(dw->sprt.sprites[i].masks);
+ }
+ // Runtime-allocated sprites (indices >= parsedCount) own their synthesized name
+ if (i >= dw->sprt.parsedCount) free((char*) dw->sprt.sprites[i].name);
+ }
+ free(dw->sprt.sprites);
+ }
+
+
+ // BGND
+ if (dw->bgnd.backgrounds) {
+ repeat(dw->bgnd.count, i) {
+ free(dw->bgnd.backgrounds[i].gms2TileIds);
+ }
+ }
+ free(dw->bgnd.backgrounds);
+
+ // PATH
+ if (dw->path.paths) {
+ repeat(dw->path.count, i) {
+ free(dw->path.paths[i].points);
+ free(dw->path.paths[i].internalPoints);
+ }
+ free(dw->path.paths);
+ }
+
+ // SCPT
+ free(dw->scpt.scripts);
+
+ // GLOB
+ free(dw->glob.codeIds);
+
+ // SHDR
+ if (dw->shdr.shaders) {
+ repeat(dw->shdr.count, i) {
+ free(dw->shdr.shaders[i].vertexAttributes);
+ }
+ free(dw->shdr.shaders);
+ }
+
+ // FONT
+ if (dw->font.fonts) {
+ repeat(dw->font.count, i) {
+ Font* font = &dw->font.fonts[i];
+ if (font->glyphs) {
+ repeat(font->glyphCount, j) {
+ free(font->glyphs[j].kerning);
+ }
+ free(font->glyphs);
+ }
+ }
+ free(dw->font.fonts);
+ }
+
+ // TMLN
+ if (dw->tmln.timelines) {
+ repeat(dw->tmln.count, i) {
+ Timeline* tl = &dw->tmln.timelines[i];
+ if (tl->moments) {
+ repeat(tl->momentCount, j) {
+ free(tl->moments[j].actions);
+ }
+ free(tl->moments);
+ }
+ }
+ free(dw->tmln.timelines);
+ }
+
+ // OBJT
+ if (dw->objt.objects) {
+ repeat(dw->objt.count, i) {
+ GameObject* obj = &dw->objt.objects[i];
+ free(obj->physicsVertices);
+ repeat(OBJT_EVENT_TYPE_COUNT, e) {
+ ObjectEventList* list = &obj->eventLists[e];
+ if (list->events) {
+ repeat(list->eventCount, j) {
+ free(list->events[j].actions);
+ }
+ free(list->events);
+ }
+ }
+ }
+ free(dw->objt.objects);
+ }
+
+ // ROOM
+ if (dw->room.rooms) {
+ repeat(dw->room.count, i) {
+ DataWin_freeRoomPayload(&dw->room.rooms[i]);
+ }
+ free(dw->room.rooms);
+ }
+
+ // TPAG
+ free(dw->tpag.items);
+
+ // CODE
+ free(dw->code.entries);
+
+ // VARI
+ free(dw->vari.variables);
+
+ // FUNC
+ free(dw->func.functions);
+ if (dw->func.codeLocals) {
+ repeat(dw->func.codeLocalsCount, i) {
+ free(dw->func.codeLocals[i].locals);
+ }
+ free(dw->func.codeLocals);
+ }
+
+ // STRG
+ free(dw->strg.strings);
+
+ // TXTR
+ if (dw->txtr.textures) {
+ repeat(dw->txtr.count, i) {
+ free(dw->txtr.textures[i].blobData);
+ }
+ free(dw->txtr.textures);
+ }
+
+ // AUDO
+ if (dw->audo.entries) {
+ repeat(dw->audo.count, i) {
+ free(dw->audo.entries[i].data);
+ }
+ free(dw->audo.entries);
+ }
+
+ // Owned buffers
+ free(dw->strgBuffer);
+ free(dw->bytecodeBuffer);
+
+ // Close the lazy-load file handle (only open when lazyLoadRooms was enabled)
+ if (dw->lazyLoadFile != nullptr) {
+ fclose(dw->lazyLoadFile);
+ dw->lazyLoadFile = nullptr;
+ }
+ free(dw->lazyLoadFilePath);
+
+ // Close the lazy audio file handle (only open when parseAudoHeadersOnly was used)
+ if (dw->lazyAudioFile != nullptr) {
+ fclose(dw->lazyAudioFile);
+ dw->lazyAudioFile = nullptr;
+ }
+
+ free(dw);
+}
+
+uint8_t* DataWin_readAudioEntryData(DataWin* dw, const AudioEntry* entry) {
+ if (dw == nullptr || entry == nullptr) return nullptr;
+ if (entry->dataSize == 0) return nullptr;
+
+ if (entry->data != nullptr) {
+ uint8_t* copy = safeMalloc(entry->dataSize);
+ memcpy(copy, entry->data, entry->dataSize);
+ return copy;
+ }
+
+ if (dw->lazyAudioFile == nullptr) return nullptr;
+ if (fseek(dw->lazyAudioFile, (long) entry->dataOffset, SEEK_SET) != 0) return nullptr;
+
+ uint8_t* buf = safeMalloc(entry->dataSize);
+ size_t got = fread(buf, 1, entry->dataSize, dw->lazyAudioFile);
+ if (got != entry->dataSize) {
+ free(buf);
+ return nullptr;
+ }
+ return buf;
+}
+
+// ===[ Lazy Room Payload ]===
+
+void DataWin_freeRoomPayload(Room* room) {
+ requireNotNull(room);
+ free(room->backgrounds);
+ room->backgrounds = nullptr;
+ free(room->views);
+ room->views = nullptr;
+ free(room->gameObjects);
+ room->gameObjects = nullptr;
+ room->gameObjectCount = 0;
+ free(room->tiles);
+ room->tiles = nullptr;
+ room->tileCount = 0;
+ if (room->layerCount != 0 && room->layers != nullptr) {
+ repeat(room->layerCount, j) {
+ RoomLayer* layer = &room->layers[j];
+ if (layer->assetsData) {
+ free(layer->assetsData->legacyTiles);
+ free(layer->assetsData->sprites);
+ free(layer->assetsData);
+ }
+ if (layer->backgroundData) free(layer->backgroundData);
+ if (layer->instancesData) {
+ free(layer->instancesData->instanceIds);
+ free(layer->instancesData);
+ }
+ if (layer->tilesData) {
+ free(layer->tilesData->tileData);
+ free(layer->tilesData);
+ }
+ }
+ }
+ free(room->layers);
+ room->layers = nullptr;
+ room->layerCount = 0;
+ room->payloadLoaded = false;
+}
+
+void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex) {
+ require(roomIndex >= 0 && dw->room.count > (uint32_t) roomIndex);
+ Room* room = &dw->room.rooms[roomIndex];
+ if (room->payloadLoaded) return;
+ requireMessage(dw->lazyLoadFile != nullptr, "DataWin_loadRoomPayload called without an open lazy-load FILE*");
+
+ FILE* f = dw->lazyLoadFile;
+ BinaryReader lazyReader = BinaryReader_create(f, dw->fileSize);
+ readRoomPayload(&lazyReader, dw, room);
+}
+
+// ===[ Dynamic Sprite Slot Allocation ]===
+
+uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex) {
+ uint32_t newIndex;
+ for (uint32_t i = startIndex; dw->sprt.count > i; i++) {
+ if (dw->sprt.sprites[i].textureCount == 0) {
+ newIndex = i;
+ goto assignName;
+ }
+ }
+ newIndex = dw->sprt.count;
+ dw->sprt.count++;
+ dw->sprt.sprites = safeRealloc(dw->sprt.sprites, dw->sprt.count * sizeof(Sprite));
+ memset(&dw->sprt.sprites[newIndex], 0, sizeof(Sprite));
+assignName:
+ // Match the native runner: set a "__newsprite" name so asset_get_index can find it.
+ // A reused slot preserves its name across glDeleteSprite's memset, so we only strdup when the slot is freshly appended (name is still NULL).
+ if (!dw->sprt.sprites[newIndex].name) {
+ char buf[32];
+ snprintf(buf, sizeof(buf), "__newsprite%u", newIndex);
+ dw->sprt.sprites[newIndex].name = strdup(buf);
+ }
+ return newIndex;
+}
+
+// ===[ Version Detection ]===
+
+bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) {
+ const DetectedFormat* f = &dw->detectedFormat;
+ if (f->major != major) return f->major > major;
+ if (f->minor != minor) return f->minor > minor;
+ if (f->release != release) return f->release > release;
+ return f->build >= build;
+}
+
+void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) {
+ if (DataWin_isVersionAtLeast(dw, major, minor, release, build)) return;
+ dw->detectedFormat.major = major;
+ dw->detectedFormat.minor = minor;
+ dw->detectedFormat.release = release;
+ dw->detectedFormat.build = build;
+}
\ No newline at end of file
diff --git a/src/data_win.h b/src/data_win.h
index 4b929bc4..18fb9b90 100644
--- a/src/data_win.h
+++ b/src/data_win.h
@@ -1,864 +1,876 @@
-#pragma once
-
-#include "common.h"
-#include
-#include
-#include
-#include
-#include
-
-#include "utils.h"
-
-// Forward declaration for progress callback
-typedef struct DataWin DataWin;
-
-typedef struct {
- bool parseGen8;
- bool parseOptn;
- bool parseLang;
- bool parseExtn;
- bool parseSond;
- bool parseAgrp;
- bool parseSprt;
- bool parseBgnd;
- bool parsePath;
- bool parseScpt;
- bool parseGlob;
- bool parseShdr;
- bool parseFont;
- bool parseTmln;
- bool parseObjt;
- bool parseRoom;
- bool parseTpag;
- bool parseCode;
- bool parseVari;
- bool parseFunc;
- bool parseStrg;
- bool parseTxtr;
- bool parseAudo;
- // If true, precise masks will be skipped when the sprite does not have a precise state set
- bool skipLoadingPreciseMasksForNonPreciseSprites;
-
- // If true, Room payloads (backgrounds, views, gameObjects, tiles, layers) are parsed on demand via DataWin_loadRoomPayload during gameplay.
- bool lazyLoadRooms;
-
- // When lazyLoadRooms is true, this list indicates which rooms should be loaded during load time instead of demand. They will also not be freed.
- StringBooleanEntry* eagerlyLoadedRooms;
-
- // Optional progress callback, called before each chunk is parsed.
- // chunkName: 4-character chunk name (e.g. "GEN8", "SPRT")
- // chunkIndex: 0-based index of the current chunk being parsed
- // totalChunks: total number of chunks in the file
- // dataWin: the DataWin being populated (earlier chunks may already be parsed)
- // userData: user-provided pointer passed through from the options
- void (*progressCallback)(const char* chunkName, int chunkIndex, int totalChunks, DataWin* dataWin, void* userData);
- void* progressCallbackUserData;
-} DataWinParserOptions;
-
-// ===[ GEN8 - General Info ]===
-typedef struct {
- uint8_t isDebuggerDisabled;
- uint8_t bytecodeVersion;
- const char* fileName;
- const char* config;
- uint32_t lastObj;
- uint32_t lastTile;
- uint32_t gameID;
- uint8_t directPlayGuid[16];
- const char* name;
- uint32_t major;
- uint32_t minor;
- uint32_t release;
- uint32_t build;
- uint32_t defaultWindowWidth;
- uint32_t defaultWindowHeight;
- uint32_t info;
- uint32_t licenseCRC32;
- uint8_t licenseMD5[16];
- uint64_t timestamp;
- const char* displayName;
- uint64_t activeTargets;
- uint64_t functionClassifications;
- int32_t steamAppID;
- uint32_t debuggerPort;
- uint32_t roomOrderCount;
- int32_t* roomOrder;
- float gms2FPS;
-} Gen8;
-
-// ===[ OPTN - Options ]===
-typedef struct {
- const char* name;
- const char* value;
-} OptnConstant;
-
-typedef struct {
- uint64_t info;
- int32_t scale;
- uint32_t windowColor;
- uint32_t colorDepth;
- uint32_t resolution;
- uint32_t frequency;
- uint32_t vertexSync;
- uint32_t priority;
- uint32_t backImage;
- uint32_t frontImage;
- uint32_t loadImage;
- uint32_t loadAlpha;
- uint32_t constantCount;
- OptnConstant* constants;
-} Optn;
-
-// ===[ LANG - Languages ]===
-typedef struct {
- const char* name;
- const char* region;
- uint32_t entryCount;
- const char** entries;
-} Language;
-
-typedef struct {
- uint32_t unknown1;
- uint32_t languageCount;
- uint32_t entryCount;
- const char** entryIds;
- Language* languages;
-} Lang;
-
-// ===[ EXTN - Extensions ]===
-typedef struct {
- const char* name;
- uint32_t id;
- uint32_t kind;
- uint32_t retType;
- const char* extName;
- uint32_t argumentCount;
- uint32_t* arguments;
-} ExtensionFunction;
-
-typedef struct {
- const char* filename;
- const char* cleanupScript;
- const char* initScript;
- uint32_t kind;
- uint32_t functionCount;
- ExtensionFunction* functions;
-} ExtensionFile;
-
-typedef struct {
- const char* folderName;
- const char* name;
- const char* className;
- uint32_t fileCount;
- ExtensionFile* files;
-} Extension;
-
-typedef struct {
- uint32_t count;
- Extension* extensions;
-} Extn;
-
-// ===[ SOND - Sounds ]===
-typedef struct {
- const char* name;
- uint32_t flags;
- const char* type;
- const char* file;
- uint32_t effects;
- float volume;
- float pitch;
- int32_t audioGroup;
- int32_t audioFile;
-} Sound;
-
-typedef struct {
- uint32_t count;
- Sound* sounds;
-} Sond;
-
-// ===[ AGRP - Audio Groups ]===
-typedef struct {
- const char* name;
-} AudioGroup;
-
-typedef struct {
- uint32_t count;
- AudioGroup* audioGroups;
-} Agrp;
-
-// ===[ SPRT - Sprites ]===
-typedef struct {
- const char* name;
- uint32_t width;
- uint32_t height;
- int32_t marginLeft;
- int32_t marginRight;
- int32_t marginBottom;
- int32_t marginTop;
- bool transparent;
- bool smooth;
- bool preload;
- uint32_t bboxMode;
- uint32_t sepMasks;
- int32_t originX;
- int32_t originY;
- uint32_t sVersion;
- uint32_t sSpriteType;
- float gms2PlaybackSpeed;
- bool gms2PlaybackSpeedType;
- bool specialType;
- uint32_t textureCount;
- int32_t* tpagIndices; // resolved TPAG indices (one per frame); -1 for unresolved
- uint32_t maskCount; // number of collision masks (one per frame, or 0)
- uint8_t** masks; // array of maskCount packed bit arrays (nullptr if none)
- // Nine-slice (GMS2 sVersion >= 3). Present iff the sprite stored a non-zero nineSliceOffset.
- bool nineSliceEnabled;
- int32_t nsLeft;
- int32_t nsTop;
- int32_t nsRight;
- int32_t nsBottom;
- uint8_t nsTileModes[5]; // order: Left, Top, Right, Bottom, Center. 0=Stretch, 1=Repeat, 2=Mirror, 3=BlankRepeat, 4=Hide
-} Sprite;
-
-typedef struct {
- uint32_t count;
- uint32_t parsedCount; // number of sprites loaded from SPRT; slots >= parsedCount are runtime-allocated and own their `name`
- Sprite* sprites;
-} Sprt;
-
-// ===[ BGND - Backgrounds ]===
-typedef struct {
- const char* name;
- bool transparent;
- bool smooth;
- bool preload;
- int32_t tpagIndex; // resolved TPAG index, -1 if unresolved
- uint32_t gms2UnknownAlways2;
- uint32_t gms2TileWidth;
- uint32_t gms2TileHeight;
- uint32_t gms2TileSeparationX;
- uint32_t gms2TileSeparationY;
- uint32_t gms2OutputBorderX;
- uint32_t gms2OutputBorderY;
- uint32_t gms2TileColumns;
- uint32_t gms2ItemsPerTileCount;
- uint32_t gms2TileCount;
- int gms2ExportedSpriteIndex;
- int64_t gms2FrameLength;
- uint32_t *gms2TileIds;
-} Background;
-
-typedef struct {
- uint32_t count;
- Background* backgrounds;
-} Bgnd;
-
-// ===[ PATH - Paths ]===
-typedef struct {
- float x;
- float y;
- float speed;
-} PathPoint;
-
-typedef struct {
- float x;
- float y;
- float speed;
- float l; // cumulative arc length from start
-} InternalPathPoint;
-
-typedef struct {
- float x;
- float y;
- float speed;
-} PathPositionResult;
-
-typedef struct {
- const char* name;
- bool isSmooth;
- bool isClosed;
- uint32_t precision;
- uint32_t pointCount;
- PathPoint* points;
- uint32_t internalPointCount;
- InternalPathPoint* internalPoints;
- float length; // total arc length
-} GamePath;
-
-typedef struct {
- uint32_t count;
- GamePath* paths;
-} PathChunk;
-
-// ===[ SCPT - Scripts ]===
-typedef struct {
- const char* name;
- int32_t codeId;
-} Script;
-
-typedef struct {
- uint32_t count;
- Script* scripts;
-} Scpt;
-
-// ===[ GLOB - Global Init Scripts ]===
-typedef struct {
- uint32_t count;
- int32_t* codeIds;
-} Glob;
-
-// ===[ SHDR - Shaders ]===
-typedef struct {
- const char* name;
- uint32_t type;
- const char* glslES_Vertex;
- const char* glslES_Fragment;
- const char* glsl_Vertex;
- const char* glsl_Fragment;
- const char* hlsl9_Vertex;
- const char* hlsl9_Fragment;
- uint32_t hlsl11_VertexOffset;
- uint32_t hlsl11_PixelOffset;
- uint32_t vertexAttributeCount;
- const char** vertexAttributes;
- int32_t version;
- uint32_t pssl_VertexOffset;
- uint32_t pssl_VertexLen;
- uint32_t pssl_PixelOffset;
- uint32_t pssl_PixelLen;
- uint32_t cgVita_VertexOffset;
- uint32_t cgVita_VertexLen;
- uint32_t cgVita_PixelOffset;
- uint32_t cgVita_PixelLen;
- uint32_t cgPS3_VertexOffset;
- uint32_t cgPS3_VertexLen;
- uint32_t cgPS3_PixelOffset;
- uint32_t cgPS3_PixelLen;
-} Shader;
-
-typedef struct {
- uint32_t count;
- Shader* shaders;
-} Shdr;
-
-// ===[ FONT - Fonts ]===
-typedef struct {
- int16_t character;
- int16_t shiftModifier;
-} KerningPair;
-
-typedef struct {
- uint16_t character;
- uint16_t sourceX;
- uint16_t sourceY;
- uint16_t sourceWidth;
- uint16_t sourceHeight;
- int16_t shift;
- int16_t offset;
- uint16_t kerningCount;
- KerningPair* kerning;
-} FontGlyph;
-
-typedef struct {
- const char* name;
- const char* displayName;
- uint32_t emSize;
- bool bold;
- bool italic;
- uint16_t rangeStart;
- uint8_t charset;
- uint8_t antiAliasing;
- uint32_t rangeEnd;
- int32_t tpagIndex; // resolved TPAG index, -1 if unresolved
- float scaleX;
- float scaleY;
- int32_t ascenderOffset; // bytecodeVersion >= 17 only
- uint32_t ascender; // GMS 2022.2+ (0 when absent)
- uint32_t sdfSpread; // GMS 2023.2 nonLTS+ (0 when absent)
- uint32_t lineHeight; // GMS 2023.6+ (0 when absent)
- bool hasAscender;
- bool hasSDFSpread;
- bool hasLineHeight;
- uint32_t glyphCount;
- FontGlyph* glyphs;
- uint32_t maxGlyphHeight; // Computed after glyph parse: max sourceHeight across glyphs; HTML5 runner uses this for line stride (see yyFont.TextHeight)
- // ASCII fast-path lookup: glyphLUT[ch] for ch < 128, populated by Font_buildGlyphLUT after glyphs[] is filled.
- // Lets TextUtils_findGlyph skip the linear scan over glyphs[] for the (overwhelmingly common) ASCII case.
- FontGlyph* glyphLUT[128];
- // Sprite font fields (only valid when isSpriteFont is true)
- bool isSpriteFont;
- int32_t spriteIndex; // source sprite index (-1 for regular fonts)
-} Font;
-
-// Builds the ASCII fast-path lookup table from font->glyphs. Call after glyphs[] is fully populated.
-static inline void Font_buildGlyphLUT(Font* font) {
- memset(font->glyphLUT, 0, sizeof(font->glyphLUT));
- repeat(font->glyphCount, i) {
- FontGlyph* g = &font->glyphs[i];
- if (128 > g->character && font->glyphLUT[g->character] == nullptr) {
- font->glyphLUT[g->character] = g;
- }
- }
-}
-
-typedef struct {
- uint32_t count;
- Font* fonts;
-} FontChunk;
-
-// ===[ EventAction (shared by TMLN and OBJT) ]===
-typedef struct {
- uint32_t libID;
- uint32_t id;
- uint32_t kind;
- bool useRelative;
- bool isQuestion;
- bool useApplyTo;
- uint32_t exeType;
- const char* actionName;
- int32_t codeId;
- uint32_t argumentCount;
- int32_t who;
- bool relative;
- bool isNot;
- uint32_t unknownAlwaysZero;
-} EventAction;
-
-// ===[ TMLN - Timelines ]===
-typedef struct {
- uint32_t step;
- uint32_t actionCount;
- EventAction* actions;
-} TimelineMoment;
-
-typedef struct {
- const char* name;
- uint32_t momentCount;
- TimelineMoment* moments;
-} Timeline;
-
-typedef struct {
- uint32_t count;
- Timeline* timelines;
-} Tmln;
-
-// ===[ OBJT - Game Objects ]===
-#define OBJT_EVENT_TYPE_COUNT 15
-
-typedef struct {
- uint32_t eventSubtype;
- uint32_t actionCount;
- EventAction* actions;
-} ObjectEvent;
-
-typedef struct {
- uint32_t eventCount;
- ObjectEvent* events;
-} ObjectEventList;
-
-typedef struct {
- float x;
- float y;
-} PhysicsVertex;
-
-typedef struct {
- const char* name;
- int32_t spriteId;
- bool visible;
- bool managed; // GMS 2022.5+
- bool solid;
- int32_t depth;
- bool persistent;
- int32_t parentId;
- int32_t textureMaskId;
- bool usesPhysics;
- bool isSensor;
- uint32_t collisionShape;
- float density;
- float restitution;
- uint32_t group;
- float linearDamping;
- float angularDamping;
- int32_t physicsVertexCount;
- float friction;
- bool awake;
- bool kinematic;
- PhysicsVertex* physicsVertices;
- ObjectEventList eventLists[OBJT_EVENT_TYPE_COUNT];
-} GameObject;
-
-typedef struct {
- uint32_t count;
- GameObject* objects;
-} Objt;
-
-// ===[ ROOM - Rooms ]===
-typedef struct {
- bool enabled;
- bool foreground;
- int32_t backgroundDefinition;
- int32_t x;
- int32_t y;
- int32_t tileX;
- int32_t tileY;
- int32_t speedX;
- int32_t speedY;
- bool stretch;
-} RoomBackground;
-
-typedef struct {
- bool enabled;
- int32_t viewX;
- int32_t viewY;
- int32_t viewWidth;
- int32_t viewHeight;
- int32_t portX;
- int32_t portY;
- int32_t portWidth;
- int32_t portHeight;
- uint32_t borderX;
- uint32_t borderY;
- int32_t speedX;
- int32_t speedY;
- int32_t objectId;
-} RoomView;
-
-typedef struct {
- int32_t x;
- int32_t y;
- int32_t objectDefinition;
- uint32_t instanceID;
- int32_t creationCode;
- float scaleX;
- float scaleY;
- float imageSpeed; // GMS >= 2.2.2.302 only, otherwise 0.0f
- int32_t imageIndex; // GMS >= 2.2.2.302 only, otherwise 0
- uint32_t color;
- float rotation;
- int32_t preCreateCode;
-} RoomGameObject;
-
-typedef struct {
- int32_t x;
- int32_t y;
- bool useSpriteDefinition;
- int32_t backgroundDefinition;
- int32_t sourceX;
- int32_t sourceY;
- uint32_t width;
- uint32_t height;
- int32_t tileDepth;
- uint32_t instanceID;
- float scaleX;
- float scaleY;
- uint32_t color;
-} RoomTile;
-
-enum RoomLayerType : uint32_t
-{
- RoomLayerType_Path = 0,
- RoomLayerType_Background = 1,
- RoomLayerType_Instances = 2,
- RoomLayerType_Assets = 3,
- RoomLayerType_Tiles = 4,
- RoomLayerType_Effect = 6,
- RoomLayerType_Path2 = 7
-};
-
-typedef struct {
- const char* name;
- int32_t spriteIndex; // Direct index into SPRT chunk
- int32_t x;
- int32_t y;
- float scaleX;
- float scaleY;
- uint32_t color;
- float animationSpeed;
- uint32_t animationSpeedType;
- float frameIndex;
- float rotation;
-} SpriteInstance;
-
-typedef struct {
- uint32_t legacyTileCount;
- RoomTile *legacyTiles;
- uint32_t spriteCount;
- SpriteInstance *sprites;
-} RoomLayerAssetsData;
-
-typedef struct {
- bool visible;
- bool foreground;
- int32_t spriteIndex; // into SPRT (-1 = none)
- bool hTiled;
- bool vTiled;
- bool stretch;
- uint32_t color;
- float firstFrame;
- float animSpeed;
- uint32_t animSpeedType;
-} RoomLayerBackgroundData;
-
-typedef struct {
- uint32_t instanceCount;
- uint32_t* instanceIds;
-} RoomLayerInstancesData;
-
-typedef struct {
- int32_t backgroundIndex; // tileset (BGND index)
- uint32_t tilesX; // grid width in tiles
- uint32_t tilesY; // grid height in tiles
- uint32_t* tileData; // flat array of tilesX * tilesY tile values (row-major)
-} RoomLayerTilesData;
-
-typedef struct {
- const char* name;
- uint32_t id;
- uint32_t type;
- int32_t depth;
- float xOffset;
- float yOffset;
- float hSpeed;
- float vSpeed;
- bool visible;
- RoomLayerAssetsData *assetsData;
- RoomLayerBackgroundData *backgroundData;
- RoomLayerInstancesData *instancesData;
- RoomLayerTilesData *tilesData;
-} RoomLayer;
-
-typedef struct {
- // Scalar header: always valid regardless of payloadLoaded.
- const char* name;
- const char* caption;
- uint32_t width;
- uint32_t height;
- uint32_t speed;
- bool persistent;
- uint32_t backgroundColor;
- bool drawBackgroundColor;
- int32_t creationCodeId;
- uint32_t flags;
- bool world;
- uint32_t top;
- uint32_t left;
- uint32_t right;
- uint32_t bottom;
- float gravityX;
- float gravityY;
- float metersPerPixel;
-
- // Lazy-load offsets: absolute file offsets to the PointerList head for each payload section.
- // Captured during the header pass of parseROOM so DataWin_loadRoomPayload can seek directly.
- uint32_t backgroundsFileOffset;
- uint32_t viewsFileOffset;
- uint32_t gameObjectsFileOffset;
- uint32_t tilesFileOffset;
- uint32_t layersFileOffset; // 0 if pre-GMS2
- bool payloadLoaded;
- bool eagerlyLoaded; // set if this room's name matched DataWinParserOptions.eagerlyLoadedRooms; payload is preserved across transitions
-
- // Payload: valid only when payloadLoaded is true. Zeroed/null otherwise. Backgrounds/views point to a heap array of 8 entries when loaded.
- RoomBackground* backgrounds;
- RoomView* views;
- uint32_t gameObjectCount;
- RoomGameObject* gameObjects;
- uint32_t tileCount;
- RoomTile* tiles;
- uint32_t layerCount;
- RoomLayer* layers;
-} Room;
-
-typedef struct {
- uint32_t count;
- Room* rooms;
-} RoomChunk;
-
-// ===[ TPAG - Texture Page Items ]===
-typedef struct {
- uint16_t sourceX;
- uint16_t sourceY;
- uint16_t sourceWidth;
- uint16_t sourceHeight;
- uint16_t targetX;
- uint16_t targetY;
- uint16_t targetWidth;
- uint16_t targetHeight;
- uint16_t boundingWidth;
- uint16_t boundingHeight;
- int16_t texturePageId;
-} TexturePageItem;
-
-typedef struct {
- uint32_t count;
- TexturePageItem* items;
-} Tpag;
-
-// ===[ CODE - Code Entries ]===
-typedef struct {
- const char* name;
- uint32_t length;
- uint16_t localsCount;
- uint16_t argumentsCount;
- uint32_t bytecodeAbsoluteOffset;
- uint32_t offset;
-} CodeEntry;
-
-typedef struct {
- uint32_t count;
- CodeEntry* entries;
-} Code;
-
-// ===[ VARI - Variables ]===
-typedef struct {
- const char* name;
- int32_t instanceType;
- int32_t varID;
- uint32_t occurrences;
- uint32_t firstAddress;
- int16_t builtinVarId; // Pre-resolved enum ID for built-in variables (varID == -6), -1 otherwise
-} Variable;
-
-typedef struct {
- uint32_t varCount1;
- uint32_t varCount2;
- uint32_t maxLocalVarCount;
- uint32_t variableCount;
- Variable* variables;
-} Vari;
-
-// ===[ FUNC - Functions & Code Locals ]===
-typedef struct {
- const char* name;
- uint32_t occurrences;
- uint32_t firstAddress;
-} Function;
-
-typedef struct {
- // UndertaleModTool calls this field "Index", but that's because that's how it seemingly worked in pre-bytecode version 17
- // After bytecode version 17+, this has shown that this is actually the varID of the local variable (it matches the Variable.varID)
- uint32_t varID;
- const char* name;
-} LocalVar;
-
-typedef struct {
- const char* name;
- uint32_t localVarCount;
- LocalVar* locals;
-} CodeLocals;
-
-typedef struct {
- uint32_t functionCount;
- Function* functions;
- uint32_t codeLocalsCount;
- CodeLocals* codeLocals;
-} Func;
-
-// ===[ STRG - Strings ]===
-typedef struct {
- uint32_t count;
- const char** strings; // pointers into strgBuffer
-} Strg;
-
-// ===[ TXTR - Embedded Textures ]===
-typedef struct {
- uint32_t scaled;
- uint32_t generatedMips; // GMS 2.0.6+: number of generated mipmaps (0 for GMS 1.x)
- uint32_t textureBlockSize; // GMS 2022.3+: size of the texture block (0 for older versions)
- int32_t textureWidth; // GMS 2022.9+
- int32_t textureHeight; // GMS 2022.9+
- int32_t indexInGroup; // GMS 2022.9+
- uint32_t blobOffset; // absolute file offset to PNG data
- uint32_t blobSize; // computed size of blob data
- uint8_t* blobData; // owned copy of PNG data
-} Texture;
-
-typedef struct {
- uint32_t count;
- Texture* textures;
-} Txtr;
-
-// ===[ AUDO - Embedded Audio ]===
-typedef struct {
- uint32_t dataOffset; // absolute file offset to audio data
- uint32_t dataSize; // length of audio data
- uint8_t* data; // owned copy of audio data
-} AudioEntry;
-
-typedef struct {
- uint32_t count;
- AudioEntry* entries;
-} Audo;
-
-// ===[ Detected Format ]===
-// The effective GMS version after heuristic detection. GEN8.version is unreliable since GM:S 2,
-// so chunk parsers probe the data and bump these fields upward when they detect newer-format features.
-typedef struct {
- uint32_t major;
- uint32_t minor;
- uint32_t release;
- uint32_t build;
-} DetectedFormat;
-
-// ===[ Top-level DataWin container ]===
-typedef struct DataWin {
- uint8_t* strgBuffer; // owned copy of STRG chunk raw data
- // Absolute file offset of strgBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets
- size_t strgBufferBase;
-
- uint8_t* bytecodeBuffer; // owned copy of CODE bytecode blob
- // Absolute file offset of bytecodeBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets
- size_t bytecodeBufferBase;
-
- Gen8 gen8;
- Optn optn;
- Lang lang;
- Extn extn;
- Sond sond;
- Agrp agrp;
- Sprt sprt;
- Bgnd bgnd;
- PathChunk path;
- Scpt scpt;
- Glob glob;
- Shdr shdr;
- FontChunk font;
- Tmln tmln;
- Objt objt;
- RoomChunk room;
- // DAFL is empty, no field needed
- Tpag tpag;
- Code code;
- Vari vari;
- Func func;
- Strg strg;
- Txtr txtr;
- Audo audo;
-
- DetectedFormat detectedFormat;
-
- // Held open across the whole session when DataWinParserOptions.lazyLoadRooms is true.
- // Used by DataWin_loadRoomPayload to satisfy on-demand room payload reads.
- // nullptr when lazy loading is disabled. Closed by DataWin_free.
- FILE* lazyLoadFile;
- char* lazyLoadFilePath; // owned strdup of the original file path, for diagnostics
- bool lazyLoadRooms; // mirrors the parser option so Runner can branch without re-reading options
-} DataWin;
-
-DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options);
-void DataWin_free(DataWin* dataWin);
-void DataWin_printDebugSummary(DataWin* dataWin);
-// Lazy room payload management. DataWin_loadRoomPayload is a no-op when the payload is already loaded.
-void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex);
-void DataWin_freeRoomPayload(Room* room);
-// Finds a reusable dynamic Sprite slot (textureCount == 0) at or above `startIndex`, or appends a new one.
-uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex);
-// Compares the detected effective GMS version (not the raw GEN8 version) against a lower bound.
-// Returns true if the detected version >= (major, minor, release, build).
-//
-// Mirrors UndertaleModTool's IsVersionAtLeast.
-bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build);
-// Raises the detected effective version to at least (major, minor, release, build). No-op if the detected version is already >= the target.
-void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build);
-void GamePath_computeInternal(GamePath* path);
-PathPositionResult GamePath_getPosition(GamePath* path, float t);
+#pragma once
+
+#include "common.h"
+#include
+#include
+#include
+#include
+#include
+
+#include "utils.h"
+
+// Forward declaration for progress callback
+typedef struct DataWin DataWin;
+
+typedef struct {
+ bool parseGen8;
+ bool parseOptn;
+ bool parseLang;
+ bool parseExtn;
+ bool parseSond;
+ bool parseAgrp;
+ bool parseSprt;
+ bool parseBgnd;
+ bool parsePath;
+ bool parseScpt;
+ bool parseGlob;
+ bool parseShdr;
+ bool parseFont;
+ bool parseTmln;
+ bool parseObjt;
+ bool parseRoom;
+ bool parseTpag;
+ bool parseCode;
+ bool parseVari;
+ bool parseFunc;
+ bool parseStrg;
+ bool parseTxtr;
+ bool parseAudo;
+ bool parseAudoHeadersOnly;
+ // If true, precise masks will be skipped when the sprite does not have a precise state set
+ bool skipLoadingPreciseMasksForNonPreciseSprites;
+
+ // If true, Room payloads (backgrounds, views, gameObjects, tiles, layers) are parsed on demand via DataWin_loadRoomPayload during gameplay.
+ bool lazyLoadRooms;
+
+ // When lazyLoadRooms is true, this list indicates which rooms should be loaded during load time instead of demand. They will also not be freed.
+ StringBooleanEntry* eagerlyLoadedRooms;
+
+ // Optional progress callback, called before each chunk is parsed.
+ // chunkName: 4-character chunk name (e.g. "GEN8", "SPRT")
+ // chunkIndex: 0-based index of the current chunk being parsed
+ // totalChunks: total number of chunks in the file
+ // dataWin: the DataWin being populated (earlier chunks may already be parsed)
+ // userData: user-provided pointer passed through from the options
+ void (*progressCallback)(const char* chunkName, int chunkIndex, int totalChunks, DataWin* dataWin, void* userData);
+ void* progressCallbackUserData;
+} DataWinParserOptions;
+
+// ===[ GEN8 - General Info ]===
+typedef struct {
+ uint8_t isDebuggerDisabled;
+ uint8_t bytecodeVersion;
+ const char* fileName;
+ const char* config;
+ uint32_t lastObj;
+ uint32_t lastTile;
+ uint32_t gameID;
+ uint8_t directPlayGuid[16];
+ const char* name;
+ uint32_t major;
+ uint32_t minor;
+ uint32_t release;
+ uint32_t build;
+ uint32_t defaultWindowWidth;
+ uint32_t defaultWindowHeight;
+ uint32_t info;
+ uint32_t licenseCRC32;
+ uint8_t licenseMD5[16];
+ uint64_t timestamp;
+ const char* displayName;
+ uint64_t activeTargets;
+ uint64_t functionClassifications;
+ int32_t steamAppID;
+ uint32_t debuggerPort;
+ uint32_t roomOrderCount;
+ int32_t* roomOrder;
+ float gms2FPS;
+} Gen8;
+
+// ===[ OPTN - Options ]===
+typedef struct {
+ const char* name;
+ const char* value;
+} OptnConstant;
+
+typedef struct {
+ uint64_t info;
+ int32_t scale;
+ uint32_t windowColor;
+ uint32_t colorDepth;
+ uint32_t resolution;
+ uint32_t frequency;
+ uint32_t vertexSync;
+ uint32_t priority;
+ uint32_t backImage;
+ uint32_t frontImage;
+ uint32_t loadImage;
+ uint32_t loadAlpha;
+ uint32_t constantCount;
+ OptnConstant* constants;
+} Optn;
+
+// ===[ LANG - Languages ]===
+typedef struct {
+ const char* name;
+ const char* region;
+ uint32_t entryCount;
+ const char** entries;
+} Language;
+
+typedef struct {
+ uint32_t unknown1;
+ uint32_t languageCount;
+ uint32_t entryCount;
+ const char** entryIds;
+ Language* languages;
+} Lang;
+
+// ===[ EXTN - Extensions ]===
+typedef struct {
+ const char* name;
+ uint32_t id;
+ uint32_t kind;
+ uint32_t retType;
+ const char* extName;
+ uint32_t argumentCount;
+ uint32_t* arguments;
+} ExtensionFunction;
+
+typedef struct {
+ const char* filename;
+ const char* cleanupScript;
+ const char* initScript;
+ uint32_t kind;
+ uint32_t functionCount;
+ ExtensionFunction* functions;
+} ExtensionFile;
+
+typedef struct {
+ const char* folderName;
+ const char* name;
+ const char* className;
+ uint32_t fileCount;
+ ExtensionFile* files;
+} Extension;
+
+typedef struct {
+ uint32_t count;
+ Extension* extensions;
+} Extn;
+
+// ===[ SOND - Sounds ]===
+typedef struct {
+ const char* name;
+ uint32_t flags;
+ const char* type;
+ const char* file;
+ uint32_t effects;
+ float volume;
+ float pitch;
+ int32_t audioGroup;
+ int32_t audioFile;
+} Sound;
+
+typedef struct {
+ uint32_t count;
+ Sound* sounds;
+} Sond;
+
+// ===[ AGRP - Audio Groups ]===
+typedef struct {
+ const char* name;
+} AudioGroup;
+
+typedef struct {
+ uint32_t count;
+ AudioGroup* audioGroups;
+} Agrp;
+
+// ===[ SPRT - Sprites ]===
+typedef struct {
+ const char* name;
+ uint32_t width;
+ uint32_t height;
+ int32_t marginLeft;
+ int32_t marginRight;
+ int32_t marginBottom;
+ int32_t marginTop;
+ bool transparent;
+ bool smooth;
+ bool preload;
+ uint32_t bboxMode;
+ uint32_t sepMasks;
+ int32_t originX;
+ int32_t originY;
+ uint32_t sVersion;
+ uint32_t sSpriteType;
+ float gms2PlaybackSpeed;
+ bool gms2PlaybackSpeedType;
+ bool specialType;
+ uint32_t textureCount;
+ int32_t* tpagIndices; // resolved TPAG indices (one per frame); -1 for unresolved
+ uint32_t maskCount; // number of collision masks (one per frame, or 0)
+ uint8_t** masks; // array of maskCount packed bit arrays (nullptr if none)
+ // Nine-slice (GMS2 sVersion >= 3). Present iff the sprite stored a non-zero nineSliceOffset.
+ bool nineSliceEnabled;
+ int32_t nsLeft;
+ int32_t nsTop;
+ int32_t nsRight;
+ int32_t nsBottom;
+ uint8_t nsTileModes[5]; // order: Left, Top, Right, Bottom, Center. 0=Stretch, 1=Repeat, 2=Mirror, 3=BlankRepeat, 4=Hide
+} Sprite;
+
+typedef struct {
+ uint32_t count;
+ uint32_t parsedCount; // number of sprites loaded from SPRT; slots >= parsedCount are runtime-allocated and own their `name`
+ Sprite* sprites;
+} Sprt;
+
+// ===[ BGND - Backgrounds ]===
+typedef struct {
+ const char* name;
+ bool transparent;
+ bool smooth;
+ bool preload;
+ int32_t tpagIndex; // resolved TPAG index, -1 if unresolved
+ uint32_t gms2UnknownAlways2;
+ uint32_t gms2TileWidth;
+ uint32_t gms2TileHeight;
+ uint32_t gms2TileSeparationX;
+ uint32_t gms2TileSeparationY;
+ uint32_t gms2OutputBorderX;
+ uint32_t gms2OutputBorderY;
+ uint32_t gms2TileColumns;
+ uint32_t gms2ItemsPerTileCount;
+ uint32_t gms2TileCount;
+ int gms2ExportedSpriteIndex;
+ int64_t gms2FrameLength;
+ uint32_t *gms2TileIds;
+} Background;
+
+typedef struct {
+ uint32_t count;
+ Background* backgrounds;
+} Bgnd;
+
+// ===[ PATH - Paths ]===
+typedef struct {
+ float x;
+ float y;
+ float speed;
+} PathPoint;
+
+typedef struct {
+ float x;
+ float y;
+ float speed;
+ float l; // cumulative arc length from start
+} InternalPathPoint;
+
+typedef struct {
+ float x;
+ float y;
+ float speed;
+} PathPositionResult;
+
+typedef struct {
+ const char* name;
+ bool isSmooth;
+ bool isClosed;
+ uint32_t precision;
+ uint32_t pointCount;
+ PathPoint* points;
+ uint32_t internalPointCount;
+ InternalPathPoint* internalPoints;
+ float length; // total arc length
+} GamePath;
+
+typedef struct {
+ uint32_t count;
+ GamePath* paths;
+} PathChunk;
+
+// ===[ SCPT - Scripts ]===
+typedef struct {
+ const char* name;
+ int32_t codeId;
+} Script;
+
+typedef struct {
+ uint32_t count;
+ Script* scripts;
+} Scpt;
+
+// ===[ GLOB - Global Init Scripts ]===
+typedef struct {
+ uint32_t count;
+ int32_t* codeIds;
+} Glob;
+
+// ===[ SHDR - Shaders ]===
+typedef struct {
+ const char* name;
+ uint32_t type;
+ const char* glslES_Vertex;
+ const char* glslES_Fragment;
+ const char* glsl_Vertex;
+ const char* glsl_Fragment;
+ const char* hlsl9_Vertex;
+ const char* hlsl9_Fragment;
+ uint32_t hlsl11_VertexOffset;
+ uint32_t hlsl11_PixelOffset;
+ uint32_t vertexAttributeCount;
+ const char** vertexAttributes;
+ int32_t version;
+ uint32_t pssl_VertexOffset;
+ uint32_t pssl_VertexLen;
+ uint32_t pssl_PixelOffset;
+ uint32_t pssl_PixelLen;
+ uint32_t cgVita_VertexOffset;
+ uint32_t cgVita_VertexLen;
+ uint32_t cgVita_PixelOffset;
+ uint32_t cgVita_PixelLen;
+ uint32_t cgPS3_VertexOffset;
+ uint32_t cgPS3_VertexLen;
+ uint32_t cgPS3_PixelOffset;
+ uint32_t cgPS3_PixelLen;
+} Shader;
+
+typedef struct {
+ uint32_t count;
+ Shader* shaders;
+} Shdr;
+
+// ===[ FONT - Fonts ]===
+typedef struct {
+ int16_t character;
+ int16_t shiftModifier;
+} KerningPair;
+
+typedef struct {
+ uint16_t character;
+ uint16_t sourceX;
+ uint16_t sourceY;
+ uint16_t sourceWidth;
+ uint16_t sourceHeight;
+ int16_t shift;
+ int16_t offset;
+ uint16_t kerningCount;
+ KerningPair* kerning;
+} FontGlyph;
+
+typedef struct {
+ const char* name;
+ const char* displayName;
+ uint32_t emSize;
+ bool bold;
+ bool italic;
+ uint16_t rangeStart;
+ uint8_t charset;
+ uint8_t antiAliasing;
+ uint32_t rangeEnd;
+ int32_t tpagIndex; // resolved TPAG index, -1 if unresolved
+ float scaleX;
+ float scaleY;
+ int32_t ascenderOffset; // bytecodeVersion >= 17 only
+ uint32_t ascender; // GMS 2022.2+ (0 when absent)
+ uint32_t sdfSpread; // GMS 2023.2 nonLTS+ (0 when absent)
+ uint32_t lineHeight; // GMS 2023.6+ (0 when absent)
+ bool hasAscender;
+ bool hasSDFSpread;
+ bool hasLineHeight;
+ uint32_t glyphCount;
+ FontGlyph* glyphs;
+ uint32_t maxGlyphHeight; // Computed after glyph parse: max sourceHeight across glyphs; HTML5 runner uses this for line stride (see yyFont.TextHeight)
+ // ASCII fast-path lookup: glyphLUT[ch] for ch < 128, populated by Font_buildGlyphLUT after glyphs[] is filled.
+ // Lets TextUtils_findGlyph skip the linear scan over glyphs[] for the (overwhelmingly common) ASCII case.
+ FontGlyph* glyphLUT[128];
+ // Sprite font fields (only valid when isSpriteFont is true)
+ bool isSpriteFont;
+ int32_t spriteIndex; // source sprite index (-1 for regular fonts)
+} Font;
+
+// Builds the ASCII fast-path lookup table from font->glyphs. Call after glyphs[] is fully populated.
+static inline void Font_buildGlyphLUT(Font* font) {
+ memset(font->glyphLUT, 0, sizeof(font->glyphLUT));
+ repeat(font->glyphCount, i) {
+ FontGlyph* g = &font->glyphs[i];
+ if (128 > g->character && font->glyphLUT[g->character] == nullptr) {
+ font->glyphLUT[g->character] = g;
+ }
+ }
+}
+
+typedef struct {
+ uint32_t count;
+ Font* fonts;
+} FontChunk;
+
+// ===[ EventAction (shared by TMLN and OBJT) ]===
+typedef struct {
+ uint32_t libID;
+ uint32_t id;
+ uint32_t kind;
+ bool useRelative;
+ bool isQuestion;
+ bool useApplyTo;
+ uint32_t exeType;
+ const char* actionName;
+ int32_t codeId;
+ uint32_t argumentCount;
+ int32_t who;
+ bool relative;
+ bool isNot;
+ uint32_t unknownAlwaysZero;
+} EventAction;
+
+// ===[ TMLN - Timelines ]===
+typedef struct {
+ uint32_t step;
+ uint32_t actionCount;
+ EventAction* actions;
+} TimelineMoment;
+
+typedef struct {
+ const char* name;
+ uint32_t momentCount;
+ TimelineMoment* moments;
+} Timeline;
+
+typedef struct {
+ uint32_t count;
+ Timeline* timelines;
+} Tmln;
+
+// ===[ OBJT - Game Objects ]===
+#define OBJT_EVENT_TYPE_COUNT 15
+
+typedef struct {
+ uint32_t eventSubtype;
+ uint32_t actionCount;
+ EventAction* actions;
+} ObjectEvent;
+
+typedef struct {
+ uint32_t eventCount;
+ ObjectEvent* events;
+} ObjectEventList;
+
+typedef struct {
+ float x;
+ float y;
+} PhysicsVertex;
+
+typedef struct {
+ const char* name;
+ int32_t spriteId;
+ bool visible;
+ bool managed; // GMS 2022.5+
+ bool solid;
+ int32_t depth;
+ bool persistent;
+ int32_t parentId;
+ int32_t textureMaskId;
+ bool usesPhysics;
+ bool isSensor;
+ uint32_t collisionShape;
+ float density;
+ float restitution;
+ uint32_t group;
+ float linearDamping;
+ float angularDamping;
+ int32_t physicsVertexCount;
+ float friction;
+ bool awake;
+ bool kinematic;
+ PhysicsVertex* physicsVertices;
+ ObjectEventList eventLists[OBJT_EVENT_TYPE_COUNT];
+} GameObject;
+
+typedef struct {
+ uint32_t count;
+ GameObject* objects;
+} Objt;
+
+// ===[ ROOM - Rooms ]===
+typedef struct {
+ bool enabled;
+ bool foreground;
+ int32_t backgroundDefinition;
+ int32_t x;
+ int32_t y;
+ int32_t tileX;
+ int32_t tileY;
+ int32_t speedX;
+ int32_t speedY;
+ bool stretch;
+} RoomBackground;
+
+typedef struct {
+ bool enabled;
+ int32_t viewX;
+ int32_t viewY;
+ int32_t viewWidth;
+ int32_t viewHeight;
+ int32_t portX;
+ int32_t portY;
+ int32_t portWidth;
+ int32_t portHeight;
+ uint32_t borderX;
+ uint32_t borderY;
+ int32_t speedX;
+ int32_t speedY;
+ int32_t objectId;
+} RoomView;
+
+typedef struct {
+ int32_t x;
+ int32_t y;
+ int32_t objectDefinition;
+ uint32_t instanceID;
+ int32_t creationCode;
+ float scaleX;
+ float scaleY;
+ float imageSpeed; // GMS >= 2.2.2.302 only, otherwise 0.0f
+ int32_t imageIndex; // GMS >= 2.2.2.302 only, otherwise 0
+ uint32_t color;
+ float rotation;
+ int32_t preCreateCode;
+} RoomGameObject;
+
+typedef struct {
+ int32_t x;
+ int32_t y;
+ bool useSpriteDefinition;
+ int32_t backgroundDefinition;
+ int32_t sourceX;
+ int32_t sourceY;
+ uint32_t width;
+ uint32_t height;
+ int32_t tileDepth;
+ uint32_t instanceID;
+ float scaleX;
+ float scaleY;
+ uint32_t color;
+} RoomTile;
+
+enum RoomLayerType
+{
+ RoomLayerType_Path = 0,
+ RoomLayerType_Background = 1,
+ RoomLayerType_Instances = 2,
+ RoomLayerType_Assets = 3,
+ RoomLayerType_Tiles = 4,
+ RoomLayerType_Effect = 6,
+ RoomLayerType_Path2 = 7
+};
+
+typedef struct {
+ const char* name;
+ int32_t spriteIndex; // Direct index into SPRT chunk
+ int32_t x;
+ int32_t y;
+ float scaleX;
+ float scaleY;
+ uint32_t color;
+ float animationSpeed;
+ uint32_t animationSpeedType;
+ float frameIndex;
+ float rotation;
+} SpriteInstance;
+
+typedef struct {
+ uint32_t legacyTileCount;
+ RoomTile *legacyTiles;
+ uint32_t spriteCount;
+ SpriteInstance *sprites;
+} RoomLayerAssetsData;
+
+typedef struct {
+ bool visible;
+ bool foreground;
+ int32_t spriteIndex; // into SPRT (-1 = none)
+ bool hTiled;
+ bool vTiled;
+ bool stretch;
+ uint32_t color;
+ float firstFrame;
+ float animSpeed;
+ uint32_t animSpeedType;
+} RoomLayerBackgroundData;
+
+typedef struct {
+ uint32_t instanceCount;
+ uint32_t* instanceIds;
+} RoomLayerInstancesData;
+
+typedef struct {
+ int32_t backgroundIndex; // tileset (BGND index)
+ uint32_t tilesX; // grid width in tiles
+ uint32_t tilesY; // grid height in tiles
+ uint32_t* tileData; // flat array of tilesX * tilesY tile values (row-major)
+} RoomLayerTilesData;
+
+typedef struct {
+ const char* name;
+ uint32_t id;
+ uint32_t type;
+ int32_t depth;
+ float xOffset;
+ float yOffset;
+ float hSpeed;
+ float vSpeed;
+ bool visible;
+ RoomLayerAssetsData *assetsData;
+ RoomLayerBackgroundData *backgroundData;
+ RoomLayerInstancesData *instancesData;
+ RoomLayerTilesData *tilesData;
+} RoomLayer;
+
+typedef struct {
+ // Scalar header: always valid regardless of payloadLoaded.
+ const char* name;
+ const char* caption;
+ uint32_t width;
+ uint32_t height;
+ uint32_t speed;
+ bool persistent;
+ uint32_t backgroundColor;
+ bool drawBackgroundColor;
+ int32_t creationCodeId;
+ uint32_t flags;
+ bool world;
+ uint32_t top;
+ uint32_t left;
+ uint32_t right;
+ uint32_t bottom;
+ float gravityX;
+ float gravityY;
+ float metersPerPixel;
+
+ // Lazy-load offsets: absolute file offsets to the PointerList head for each payload section.
+ // Captured during the header pass of parseROOM so DataWin_loadRoomPayload can seek directly.
+ uint32_t backgroundsFileOffset;
+ uint32_t viewsFileOffset;
+ uint32_t gameObjectsFileOffset;
+ uint32_t tilesFileOffset;
+ uint32_t layersFileOffset; // 0 if pre-GMS2
+ bool payloadLoaded;
+ bool eagerlyLoaded; // set if this room's name matched DataWinParserOptions.eagerlyLoadedRooms; payload is preserved across transitions
+
+ // Payload: valid only when payloadLoaded is true. Zeroed/null otherwise. Backgrounds/views point to a heap array of 8 entries when loaded.
+ RoomBackground* backgrounds;
+ RoomView* views;
+ uint32_t gameObjectCount;
+ RoomGameObject* gameObjects;
+ uint32_t tileCount;
+ RoomTile* tiles;
+ uint32_t layerCount;
+ RoomLayer* layers;
+} Room;
+
+typedef struct {
+ uint32_t count;
+ Room* rooms;
+} RoomChunk;
+
+// ===[ TPAG - Texture Page Items ]===
+typedef struct {
+ uint16_t sourceX;
+ uint16_t sourceY;
+ uint16_t sourceWidth;
+ uint16_t sourceHeight;
+ uint16_t targetX;
+ uint16_t targetY;
+ uint16_t targetWidth;
+ uint16_t targetHeight;
+ uint16_t boundingWidth;
+ uint16_t boundingHeight;
+ int16_t texturePageId;
+} TexturePageItem;
+
+typedef struct {
+ uint32_t count;
+ TexturePageItem* items;
+} Tpag;
+
+// ===[ CODE - Code Entries ]===
+typedef struct {
+ const char* name;
+ uint32_t length;
+ uint16_t localsCount;
+ uint16_t argumentsCount;
+ uint32_t bytecodeAbsoluteOffset;
+ uint32_t offset;
+} CodeEntry;
+
+typedef struct {
+ uint32_t count;
+ CodeEntry* entries;
+} Code;
+
+// ===[ VARI - Variables ]===
+typedef struct {
+ const char* name;
+ int32_t instanceType;
+ int32_t varID;
+ uint32_t occurrences;
+ uint32_t firstAddress;
+ int16_t builtinVarId; // Pre-resolved enum ID for built-in variables (varID == -6), -1 otherwise
+} Variable;
+
+typedef struct {
+ uint32_t varCount1;
+ uint32_t varCount2;
+ uint32_t maxLocalVarCount;
+ uint32_t variableCount;
+ Variable* variables;
+} Vari;
+
+// ===[ FUNC - Functions & Code Locals ]===
+typedef struct {
+ const char* name;
+ uint32_t occurrences;
+ uint32_t firstAddress;
+} Function;
+
+typedef struct {
+ // UndertaleModTool calls this field "Index", but that's because that's how it seemingly worked in pre-bytecode version 17
+ // After bytecode version 17+, this has shown that this is actually the varID of the local variable (it matches the Variable.varID)
+ uint32_t varID;
+ const char* name;
+} LocalVar;
+
+typedef struct {
+ const char* name;
+ uint32_t localVarCount;
+ LocalVar* locals;
+} CodeLocals;
+
+typedef struct {
+ uint32_t functionCount;
+ Function* functions;
+ uint32_t codeLocalsCount;
+ CodeLocals* codeLocals;
+} Func;
+
+// ===[ STRG - Strings ]===
+typedef struct {
+ uint32_t count;
+ const char** strings; // pointers into strgBuffer
+} Strg;
+
+// ===[ TXTR - Embedded Textures ]===
+typedef struct {
+ uint32_t scaled;
+ uint32_t generatedMips; // GMS 2.0.6+: number of generated mipmaps (0 for GMS 1.x)
+ uint32_t textureBlockSize; // GMS 2022.3+: size of the texture block (0 for older versions)
+ int32_t textureWidth; // GMS 2022.9+
+ int32_t textureHeight; // GMS 2022.9+
+ int32_t indexInGroup; // GMS 2022.9+
+ uint32_t blobOffset; // absolute file offset to PNG data
+ uint32_t blobSize; // computed size of blob data
+ uint8_t* blobData; // owned copy of PNG data
+} Texture;
+
+typedef struct {
+ uint32_t count;
+ Texture* textures;
+} Txtr;
+
+// ===[ AUDO - Embedded Audio ]===
+typedef struct {
+ uint32_t dataOffset; // absolute file offset to audio data
+ uint32_t dataSize; // length of audio data
+ uint8_t* data; // owned copy of audio data
+} AudioEntry;
+
+typedef struct {
+ uint32_t count;
+ AudioEntry* entries;
+} Audo;
+
+// ===[ Detected Format ]===
+// The effective GMS version after heuristic detection. GEN8.version is unreliable since GM:S 2,
+// so chunk parsers probe the data and bump these fields upward when they detect newer-format features.
+typedef struct {
+ uint32_t major;
+ uint32_t minor;
+ uint32_t release;
+ uint32_t build;
+} DetectedFormat;
+
+// ===[ Top-level DataWin container ]===
+typedef struct DataWin {
+ uint8_t* strgBuffer; // owned copy of STRG chunk raw data
+ // Absolute file offset of strgBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets
+ size_t strgBufferBase;
+
+ uint8_t* bytecodeBuffer; // owned copy of CODE bytecode blob
+ // Absolute file offset of bytecodeBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets
+ size_t bytecodeBufferBase;
+
+ Gen8 gen8;
+ Optn optn;
+ Lang lang;
+ Extn extn;
+ Sond sond;
+ Agrp agrp;
+ Sprt sprt;
+ Bgnd bgnd;
+ PathChunk path;
+ Scpt scpt;
+ Glob glob;
+ Shdr shdr;
+ FontChunk font;
+ Tmln tmln;
+ Objt objt;
+ RoomChunk room;
+ // DAFL is empty, no field needed
+ Tpag tpag;
+ Code code;
+ Vari vari;
+ Func func;
+ Strg strg;
+ Txtr txtr;
+ Audo audo;
+
+ DetectedFormat detectedFormat;
+
+ // Held open across the whole session when DataWinParserOptions.lazyLoadRooms is true.
+ // Used by DataWin_loadRoomPayload to satisfy on-demand room payload reads.
+ // nullptr when lazy loading is disabled. Closed by DataWin_free.
+ FILE* lazyLoadFile;
+ char* lazyLoadFilePath; // owned strdup of the original file path, for diagnostics
+ size_t fileSize; // cached size of the DataWin, captured at parse time for lazy-load bounds checks
+ bool lazyLoadRooms; // mirrors the parser option so Runner can branch without re-reading options
+
+ // Kept open when parseAudoHeadersOnly was set. Used by the audio system to read
+ // individual audio entry bytes on demand instead of loading everything upfront.
+ // Access must be serialized — call DataWin_readAudioEntryData for thread-safe reads.
+ // Closed by DataWin_free.
+ FILE* lazyAudioFile;
+} DataWin;
+
+DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options);
+void DataWin_free(DataWin* dataWin);
+void DataWin_printDebugSummary(DataWin* dataWin);
+// Lazy room payload management. DataWin_loadRoomPayload is a no-op when the payload is already loaded.
+void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex);
+void DataWin_freeRoomPayload(Room* room);
+// Reads audio entry data on demand when parseAudoHeadersOnly was used.
+// Returns a malloc'd buffer of entry->dataSize bytes that the caller must free,
+// or NULL on failure. Safe to call from any thread.
+uint8_t* DataWin_readAudioEntryData(DataWin* dw, const AudioEntry* entry);
+// Finds a reusable dynamic Sprite slot (textureCount == 0) at or above `startIndex`, or appends a new one.
+uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex);
+// Compares the detected effective GMS version (not the raw GEN8 version) against a lower bound.
+// Returns true if the detected version >= (major, minor, release, build).
+//
+// Mirrors UndertaleModTool's IsVersionAtLeast.
+bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build);
+// Raises the detected effective version to at least (major, minor, release, build). No-op if the detected version is already >= the target.
+void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build);
+void GamePath_computeInternal(GamePath* path);
+PathPositionResult GamePath_getPosition(GamePath* path, float t);
diff --git a/src/data_win_print.c b/src/data_win_print.c
index 3a4334f5..79815960 100644
--- a/src/data_win_print.c
+++ b/src/data_win_print.c
@@ -1,278 +1,278 @@
-#include "data_win.h"
-
-#include
-#include
-
-#include "utils.h"
-
-void DataWin_printDebugSummary(DataWin* dataWin) {
- printf("===== data.win Summary =====\n\n");
-
- // GEN8
- Gen8* g = &dataWin->gen8;
- printf("-- GEN8 (General Info) --\n");
- printf(" Game Name: %s\n", g->name ? g->name : "(null)");
- printf(" Display Name: %s\n", g->displayName ? g->displayName : "(null)");
- printf(" File Name: %s\n", g->fileName ? g->fileName : "(null)");
- printf(" Config: %s\n", g->config ? g->config : "(null)");
- printf(" Bytecode Version: %u\n", g->bytecodeVersion);
- printf(" Game ID: %u\n", g->gameID);
- printf(" Version: %u.%u.%u.%u\n", g->major, g->minor, g->release, g->build);
- printf(" Window Size: %ux%u\n", g->defaultWindowWidth, g->defaultWindowHeight);
- printf(" Steam App ID: %d\n", g->steamAppID);
- printf(" Room Order: %u rooms\n", g->roomOrderCount);
- printf("\n");
-
- // OPTN
- printf("-- OPTN (Options) --\n");
- printf(" Constants: %u\n", dataWin->optn.constantCount);
- if (dataWin->optn.constantCount > 0) {
- uint32_t show = dataWin->optn.constantCount < 3 ? dataWin->optn.constantCount : 3;
- forEachIndexed(OptnConstant, constant, idx, dataWin->optn.constants, show) {
- printf(" [%u] %s = %s\n", idx, constant->name ? constant->name : "?", constant->value ? constant->value : "?");
- }
- if (dataWin->optn.constantCount > 3) printf(" ... and %u more\n", dataWin->optn.constantCount - 3);
- }
- printf("\n");
-
- // LANG
- printf("-- LANG (Languages) --\n");
- printf(" Languages: %u\n", dataWin->lang.languageCount);
- printf(" Entries: %u\n", dataWin->lang.entryCount);
- printf("\n");
-
- // EXTN
- printf("-- EXTN (Extensions) --\n");
- printf(" Extensions: %u\n", dataWin->extn.count);
- forEachIndexed(Extension, ext, idx, dataWin->extn.extensions, dataWin->extn.count) {
- printf(" [%u] %s (%u files)\n", idx, ext->name ? ext->name : "?", ext->fileCount);
- }
- printf("\n");
-
- // SOND
- printf("-- SOND (Sounds) --\n");
- printf(" Sounds: %u\n", dataWin->sond.count);
- if (dataWin->sond.count > 0) {
- uint32_t show = dataWin->sond.count < 3 ? dataWin->sond.count : 3;
- forEachIndexed(Sound, snd, idx, dataWin->sond.sounds, show) {
- printf(" [%u] %s (%s)\n", idx, snd->name ? snd->name : "?", snd->type ? snd->type : "?");
- }
- if (dataWin->sond.count > 3) printf(" ... and %u more\n", dataWin->sond.count - 3);
- }
- printf("\n");
-
- // AGRP
- printf("-- AGRP (Audio Groups) --\n");
- printf(" Audio Groups: %u\n", dataWin->agrp.count);
- forEachIndexed(AudioGroup, ag, idx, dataWin->agrp.audioGroups, dataWin->agrp.count) {
- printf(" [%u] %s\n", idx, ag->name ? ag->name : "?");
- }
- printf("\n");
-
- // SPRT
- printf("-- SPRT (Sprites) --\n");
- printf(" Sprites: %u\n", dataWin->sprt.count);
- if (dataWin->sprt.count > 0) {
- uint32_t show = dataWin->sprt.count < 3 ? dataWin->sprt.count : 3;
- forEachIndexed(Sprite, spr, idx, dataWin->sprt.sprites, show) {
- printf(" [%u] %s (%ux%u, %u frames)\n", idx, spr->name ? spr->name : "?", spr->width, spr->height, spr->textureCount);
- }
- if (dataWin->sprt.count > 3) printf(" ... and %u more\n", dataWin->sprt.count - 3);
- }
- printf("\n");
-
- // BGND
- printf("-- BGND (Backgrounds) --\n");
- printf(" Backgrounds: %u\n", dataWin->bgnd.count);
- if (dataWin->bgnd.count > 0) {
- uint32_t show = dataWin->bgnd.count < 3 ? dataWin->bgnd.count : 3;
- forEachIndexed(Background, bg, idx, dataWin->bgnd.backgrounds, show) {
- printf(" [%u] %s\n", idx, bg->name ? bg->name : "?");
- }
- if (dataWin->bgnd.count > 3) printf(" ... and %u more\n", dataWin->bgnd.count - 3);
- }
- printf("\n");
-
- // PATH
- printf("-- PATH (Paths) --\n");
- printf(" Paths: %u\n", dataWin->path.count);
- printf("\n");
-
- // SCPT
- printf("-- SCPT (Scripts) --\n");
- printf(" Scripts: %u\n", dataWin->scpt.count);
- if (dataWin->scpt.count > 0) {
- uint32_t show = dataWin->scpt.count < 3 ? dataWin->scpt.count : 3;
- forEachIndexed(Script, scr, idx, dataWin->scpt.scripts, show) {
- printf(" [%u] %s -> code[%d]\n", idx, scr->name ? scr->name : "?", scr->codeId);
- }
- if (dataWin->scpt.count > 3) printf(" ... and %u more\n", dataWin->scpt.count - 3);
- }
- printf("\n");
-
- // GLOB
- printf("-- GLOB (Global Init Scripts) --\n");
- printf(" Init Scripts: %u\n", dataWin->glob.count);
- printf("\n");
-
- // SHDR
- printf("-- SHDR (Shaders) --\n");
- printf(" Shaders: %u\n", dataWin->shdr.count);
- forEachIndexed(Shader, shdr, idx, dataWin->shdr.shaders, dataWin->shdr.count) {
- printf(" [%u] %s (version %d)\n", idx, shdr->name ? shdr->name : "?", shdr->version);
- }
- printf("\n");
-
- // FONT
- printf("-- FONT (Fonts) --\n");
- printf(" Fonts: %u\n", dataWin->font.count);
- forEachIndexed(Font, fnt, idx, dataWin->font.fonts, dataWin->font.count) {
- printf(" [%u] %s (%s, em=%u, %u glyphs)\n", idx, fnt->name ? fnt->name : "?", fnt->displayName ? fnt->displayName : "?", fnt->emSize, fnt->glyphCount);
- }
- printf("\n");
-
- // TMLN
- printf("-- TMLN (Timelines) --\n");
- printf(" Timelines: %u\n", dataWin->tmln.count);
- printf("\n");
-
- // OBJT
- printf("-- OBJT (Game Objects) --\n");
- printf(" Objects: %u\n", dataWin->objt.count);
- if (dataWin->objt.count > 0) {
- uint32_t show = dataWin->objt.count < 3 ? dataWin->objt.count : 3;
- forEachIndexed(GameObject, obj, idx, dataWin->objt.objects, show) {
- uint32_t totalEvents = 0;
- repeat(OBJT_EVENT_TYPE_COUNT, e) {
- totalEvents += obj->eventLists[e].eventCount;
- }
- printf(" [%u] %s (sprite=%d, depth=%d, %u events)\n", idx, obj->name ? obj->name : "?", obj->spriteId, obj->depth, totalEvents);
- }
- if (dataWin->objt.count > 3) printf(" ... and %u more\n", dataWin->objt.count - 3);
- }
- printf("\n");
-
- // ROOM
- printf("-- ROOM (Rooms) --\n");
- printf(" Rooms: %u\n", dataWin->room.count);
- if (dataWin->room.count > 0) {
- uint32_t show = dataWin->room.count < 3 ? dataWin->room.count : 3;
- forEachIndexed(Room, rm, idx, dataWin->room.rooms, show) {
- if (rm->payloadLoaded) {
- printf(" [%u] %s (%ux%u, %u objects, %u tiles)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height, rm->gameObjectCount, rm->tileCount);
- } else {
- // Lazy room with payload not yet loaded: gameObjectCount/tileCount would be 0 and misleading.
- printf(" [%u] %s (%ux%u, payload not loaded)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height);
- }
- }
- if (dataWin->room.count > 3) printf(" ... and %u more\n", dataWin->room.count - 3);
- }
- printf("\n");
-
- // TPAG
- printf("-- TPAG (Texture Page Items) --\n");
- printf(" Items: %u\n", dataWin->tpag.count);
- printf("\n");
-
- // CODE
- printf("-- CODE (Code Entries) --\n");
- printf(" Entries: %u\n", dataWin->code.count);
- if (dataWin->code.count > 0) {
- uint32_t show = dataWin->code.count < 3 ? dataWin->code.count : 3;
- forEachIndexed(CodeEntry, entry, idx, dataWin->code.entries, show) {
- printf(" [%u] %s (%u bytes, %u locals, %u args)\n", idx, entry->name ? entry->name : "?", entry->length, entry->localsCount, entry->argumentsCount);
- }
- if (dataWin->code.count > 3) printf(" ... and %u more\n", dataWin->code.count - 3);
- }
- printf("\n");
-
- // VARI
- printf("-- VARI (Variables) --\n");
- printf(" Variables: %u\n", dataWin->vari.variableCount);
- printf(" Max Locals: %u\n", dataWin->vari.maxLocalVarCount);
- if (dataWin->vari.variableCount > 0) {
- uint32_t show = dataWin->vari.variableCount < 3 ? dataWin->vari.variableCount : 3;
- forEachIndexed(Variable, var, idx, dataWin->vari.variables, show) {
- printf(" [%u] %s (type=%d, id=%d, %u refs)\n", idx, var->name ? var->name : "?", var->instanceType, var->varID, var->occurrences);
- }
- if (dataWin->vari.variableCount > 3) printf(" ... and %u more\n", dataWin->vari.variableCount - 3);
- }
- printf("\n");
-
- // FUNC
- printf("-- FUNC (Functions) --\n");
- printf(" Functions: %u\n", dataWin->func.functionCount);
- printf(" Code Locals: %u\n", dataWin->func.codeLocalsCount);
- if (dataWin->func.functionCount > 0) {
- uint32_t show = dataWin->func.functionCount < 3 ? dataWin->func.functionCount : 3;
- forEachIndexed(Function, fn, idx, dataWin->func.functions, show) {
- printf(" [%u] %s (%u refs)\n", idx, fn->name ? fn->name : "?", fn->occurrences);
- }
- if (dataWin->func.functionCount > 3) printf(" ... and %u more\n", dataWin->func.functionCount - 3);
- }
- printf("\n");
-
- // STRG
- printf("-- STRG (Strings) --\n");
- printf(" Strings: %u\n", dataWin->strg.count);
- if (dataWin->strg.count > 0) {
- uint32_t show = dataWin->strg.count < 5 ? dataWin->strg.count : 5;
- repeat(show, i) {
- const char* str = dataWin->strg.strings[i];
- // Truncate long strings for display
- if (str) {
- size_t len = strlen(str);
- if (len > 60) {
- printf(" [%u] \"%.60s...\" (%zu chars)\n", i, str, len);
- } else {
- printf(" [%u] \"%s\"\n", i, str);
- }
- } else {
- printf(" [%u] (null)\n", i);
- }
- }
- if (dataWin->strg.count > 5) printf(" ... and %u more\n", dataWin->strg.count - 5);
- }
- printf("\n");
-
- // TXTR
- printf("-- TXTR (Textures) --\n");
- printf(" Textures: %u\n", dataWin->txtr.count);
- if (dataWin->txtr.count > 0) {
- forEachIndexed(Texture, tex, idx, dataWin->txtr.textures, dataWin->txtr.count) {
- printf(" [%u] offset=0x%08X size=%u bytes\n", idx, tex->blobOffset, tex->blobSize);
- }
- }
- printf("\n");
-
- // AUDO
- printf("-- AUDO (Audio) --\n");
- printf(" Audio Entries: %u\n", dataWin->audo.count);
- if (dataWin->audo.count > 0) {
- uint32_t show = dataWin->audo.count < 3 ? dataWin->audo.count : 3;
- forEachIndexed(AudioEntry, ae, idx, dataWin->audo.entries, show) {
- printf(" [%u] offset=0x%08X size=%u bytes\n", idx, ae->dataOffset, ae->dataSize);
- }
- if (dataWin->audo.count > 3) printf(" ... and %u more\n", dataWin->audo.count - 3);
- }
- printf("\n");
-
- printf("-- Room Instances --\n");
- forEach(Room, room, dataWin->room.rooms, dataWin->room.count) {
- printf("Room %s\n", room->name);
-
- if (!room->payloadLoaded) {
- printf(" (payload not loaded)\n");
- continue;
- }
-
- forEachIndexed(RoomGameObject, roomGameObject, idx, room->gameObjects, room->gameObjectCount) {
- int32_t objectDefinitionId = roomGameObject->objectDefinition;
- GameObject* objectDefinition = &dataWin->objt.objects[objectDefinitionId];
- printf(" Object %d (%s, x=%d, y=%d)\n", objectDefinitionId, objectDefinition->name, roomGameObject->x, roomGameObject->y);
- }
- }
-
- // Overall summary
- printf("===== DataWin parse complete =====\n");
-}
+#include "data_win.h"
+
+#include
+#include
+
+#include "utils.h"
+
+void DataWin_printDebugSummary(DataWin* dataWin) {
+ printf("===== data.win Summary =====\n\n");
+
+ // GEN8
+ Gen8* g = &dataWin->gen8;
+ printf("-- GEN8 (General Info) --\n");
+ printf(" Game Name: %s\n", g->name ? g->name : "(null)");
+ printf(" Display Name: %s\n", g->displayName ? g->displayName : "(null)");
+ printf(" File Name: %s\n", g->fileName ? g->fileName : "(null)");
+ printf(" Config: %s\n", g->config ? g->config : "(null)");
+ printf(" Bytecode Version: %u\n", g->bytecodeVersion);
+ printf(" Game ID: %u\n", g->gameID);
+ printf(" Version: %u.%u.%u.%u\n", g->major, g->minor, g->release, g->build);
+ printf(" Window Size: %ux%u\n", g->defaultWindowWidth, g->defaultWindowHeight);
+ printf(" Steam App ID: %d\n", g->steamAppID);
+ printf(" Room Order: %u rooms\n", g->roomOrderCount);
+ printf("\n");
+
+ // OPTN
+ printf("-- OPTN (Options) --\n");
+ printf(" Constants: %u\n", dataWin->optn.constantCount);
+ if (dataWin->optn.constantCount > 0) {
+ uint32_t show = dataWin->optn.constantCount < 3 ? dataWin->optn.constantCount : 3;
+ forEachIndexed(OptnConstant, constant, idx, dataWin->optn.constants, show) {
+ printf(" [%u] %s = %s\n", idx, constant->name ? constant->name : "?", constant->value ? constant->value : "?");
+ }
+ if (dataWin->optn.constantCount > 3) printf(" ... and %u more\n", dataWin->optn.constantCount - 3);
+ }
+ printf("\n");
+
+ // LANG
+ printf("-- LANG (Languages) --\n");
+ printf(" Languages: %u\n", dataWin->lang.languageCount);
+ printf(" Entries: %u\n", dataWin->lang.entryCount);
+ printf("\n");
+
+ // EXTN
+ printf("-- EXTN (Extensions) --\n");
+ printf(" Extensions: %u\n", dataWin->extn.count);
+ forEachIndexed(Extension, ext, idx, dataWin->extn.extensions, dataWin->extn.count) {
+ printf(" [%u] %s (%u files)\n", idx, ext->name ? ext->name : "?", ext->fileCount);
+ }
+ printf("\n");
+
+ // SOND
+ printf("-- SOND (Sounds) --\n");
+ printf(" Sounds: %u\n", dataWin->sond.count);
+ if (dataWin->sond.count > 0) {
+ uint32_t show = dataWin->sond.count < 3 ? dataWin->sond.count : 3;
+ forEachIndexed(Sound, snd, idx, dataWin->sond.sounds, show) {
+ printf(" [%u] %s (%s)\n", idx, snd->name ? snd->name : "?", snd->type ? snd->type : "?");
+ }
+ if (dataWin->sond.count > 3) printf(" ... and %u more\n", dataWin->sond.count - 3);
+ }
+ printf("\n");
+
+ // AGRP
+ printf("-- AGRP (Audio Groups) --\n");
+ printf(" Audio Groups: %u\n", dataWin->agrp.count);
+ forEachIndexed(AudioGroup, ag, idx, dataWin->agrp.audioGroups, dataWin->agrp.count) {
+ printf(" [%u] %s\n", idx, ag->name ? ag->name : "?");
+ }
+ printf("\n");
+
+ // SPRT
+ printf("-- SPRT (Sprites) --\n");
+ printf(" Sprites: %u\n", dataWin->sprt.count);
+ if (dataWin->sprt.count > 0) {
+ uint32_t show = dataWin->sprt.count < 3 ? dataWin->sprt.count : 3;
+ forEachIndexed(Sprite, spr, idx, dataWin->sprt.sprites, show) {
+ printf(" [%u] %s (%ux%u, %u frames)\n", idx, spr->name ? spr->name : "?", spr->width, spr->height, spr->textureCount);
+ }
+ if (dataWin->sprt.count > 3) printf(" ... and %u more\n", dataWin->sprt.count - 3);
+ }
+ printf("\n");
+
+ // BGND
+ printf("-- BGND (Backgrounds) --\n");
+ printf(" Backgrounds: %u\n", dataWin->bgnd.count);
+ if (dataWin->bgnd.count > 0) {
+ uint32_t show = dataWin->bgnd.count < 3 ? dataWin->bgnd.count : 3;
+ forEachIndexed(Background, bg, idx, dataWin->bgnd.backgrounds, show) {
+ printf(" [%u] %s\n", idx, bg->name ? bg->name : "?");
+ }
+ if (dataWin->bgnd.count > 3) printf(" ... and %u more\n", dataWin->bgnd.count - 3);
+ }
+ printf("\n");
+
+ // PATH
+ printf("-- PATH (Paths) --\n");
+ printf(" Paths: %u\n", dataWin->path.count);
+ printf("\n");
+
+ // SCPT
+ printf("-- SCPT (Scripts) --\n");
+ printf(" Scripts: %u\n", dataWin->scpt.count);
+ if (dataWin->scpt.count > 0) {
+ uint32_t show = dataWin->scpt.count < 3 ? dataWin->scpt.count : 3;
+ forEachIndexed(Script, scr, idx, dataWin->scpt.scripts, show) {
+ printf(" [%u] %s -> code[%d]\n", idx, scr->name ? scr->name : "?", scr->codeId);
+ }
+ if (dataWin->scpt.count > 3) printf(" ... and %u more\n", dataWin->scpt.count - 3);
+ }
+ printf("\n");
+
+ // GLOB
+ printf("-- GLOB (Global Init Scripts) --\n");
+ printf(" Init Scripts: %u\n", dataWin->glob.count);
+ printf("\n");
+
+ // SHDR
+ printf("-- SHDR (Shaders) --\n");
+ printf(" Shaders: %u\n", dataWin->shdr.count);
+ forEachIndexed(Shader, shdr, idx, dataWin->shdr.shaders, dataWin->shdr.count) {
+ printf(" [%u] %s (version %d)\n", idx, shdr->name ? shdr->name : "?", shdr->version);
+ }
+ printf("\n");
+
+ // FONT
+ printf("-- FONT (Fonts) --\n");
+ printf(" Fonts: %u\n", dataWin->font.count);
+ forEachIndexed(Font, fnt, idx, dataWin->font.fonts, dataWin->font.count) {
+ printf(" [%u] %s (%s, em=%u, %u glyphs)\n", idx, fnt->name ? fnt->name : "?", fnt->displayName ? fnt->displayName : "?", fnt->emSize, fnt->glyphCount);
+ }
+ printf("\n");
+
+ // TMLN
+ printf("-- TMLN (Timelines) --\n");
+ printf(" Timelines: %u\n", dataWin->tmln.count);
+ printf("\n");
+
+ // OBJT
+ printf("-- OBJT (Game Objects) --\n");
+ printf(" Objects: %u\n", dataWin->objt.count);
+ if (dataWin->objt.count > 0) {
+ uint32_t show = dataWin->objt.count < 3 ? dataWin->objt.count : 3;
+ forEachIndexed(GameObject, obj, idx, dataWin->objt.objects, show) {
+ uint32_t totalEvents = 0;
+ repeat(OBJT_EVENT_TYPE_COUNT, e) {
+ totalEvents += obj->eventLists[e].eventCount;
+ }
+ printf(" [%u] %s (sprite=%d, depth=%d, %u events)\n", idx, obj->name ? obj->name : "?", obj->spriteId, obj->depth, totalEvents);
+ }
+ if (dataWin->objt.count > 3) printf(" ... and %u more\n", dataWin->objt.count - 3);
+ }
+ printf("\n");
+
+ // ROOM
+ printf("-- ROOM (Rooms) --\n");
+ printf(" Rooms: %u\n", dataWin->room.count);
+ if (dataWin->room.count > 0) {
+ uint32_t show = dataWin->room.count < 3 ? dataWin->room.count : 3;
+ forEachIndexed(Room, rm, idx, dataWin->room.rooms, show) {
+ if (rm->payloadLoaded) {
+ printf(" [%u] %s (%ux%u, %u objects, %u tiles)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height, rm->gameObjectCount, rm->tileCount);
+ } else {
+ // Lazy room with payload not yet loaded: gameObjectCount/tileCount would be 0 and misleading.
+ printf(" [%u] %s (%ux%u, payload not loaded)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height);
+ }
+ }
+ if (dataWin->room.count > 3) printf(" ... and %u more\n", dataWin->room.count - 3);
+ }
+ printf("\n");
+
+ // TPAG
+ printf("-- TPAG (Texture Page Items) --\n");
+ printf(" Items: %u\n", dataWin->tpag.count);
+ printf("\n");
+
+ // CODE
+ printf("-- CODE (Code Entries) --\n");
+ printf(" Entries: %u\n", dataWin->code.count);
+ if (dataWin->code.count > 0) {
+ uint32_t show = dataWin->code.count < 3 ? dataWin->code.count : 3;
+ forEachIndexed(CodeEntry, entry, idx, dataWin->code.entries, show) {
+ printf(" [%u] %s (%u bytes, %u locals, %u args)\n", idx, entry->name ? entry->name : "?", entry->length, entry->localsCount, entry->argumentsCount);
+ }
+ if (dataWin->code.count > 3) printf(" ... and %u more\n", dataWin->code.count - 3);
+ }
+ printf("\n");
+
+ // VARI
+ printf("-- VARI (Variables) --\n");
+ printf(" Variables: %u\n", dataWin->vari.variableCount);
+ printf(" Max Locals: %u\n", dataWin->vari.maxLocalVarCount);
+ if (dataWin->vari.variableCount > 0) {
+ uint32_t show = dataWin->vari.variableCount < 3 ? dataWin->vari.variableCount : 3;
+ forEachIndexed(Variable, var, idx, dataWin->vari.variables, show) {
+ printf(" [%u] %s (type=%d, id=%d, %u refs)\n", idx, var->name ? var->name : "?", var->instanceType, var->varID, var->occurrences);
+ }
+ if (dataWin->vari.variableCount > 3) printf(" ... and %u more\n", dataWin->vari.variableCount - 3);
+ }
+ printf("\n");
+
+ // FUNC
+ printf("-- FUNC (Functions) --\n");
+ printf(" Functions: %u\n", dataWin->func.functionCount);
+ printf(" Code Locals: %u\n", dataWin->func.codeLocalsCount);
+ if (dataWin->func.functionCount > 0) {
+ uint32_t show = dataWin->func.functionCount < 3 ? dataWin->func.functionCount : 3;
+ forEachIndexed(Function, fn, idx, dataWin->func.functions, show) {
+ printf(" [%u] %s (%u refs)\n", idx, fn->name ? fn->name : "?", fn->occurrences);
+ }
+ if (dataWin->func.functionCount > 3) printf(" ... and %u more\n", dataWin->func.functionCount - 3);
+ }
+ printf("\n");
+
+ // STRG
+ printf("-- STRG (Strings) --\n");
+ printf(" Strings: %u\n", dataWin->strg.count);
+ if (dataWin->strg.count > 0) {
+ uint32_t show = dataWin->strg.count < 5 ? dataWin->strg.count : 5;
+ repeat(show, i) {
+ const char* str = dataWin->strg.strings[i];
+ // Truncate long strings for display
+ if (str) {
+ size_t len = strlen(str);
+ if (len > 60) {
+ printf(" [%u] \"%.60s...\" (%zu chars)\n", i, str, len);
+ } else {
+ printf(" [%u] \"%s\"\n", i, str);
+ }
+ } else {
+ printf(" [%u] (null)\n", i);
+ }
+ }
+ if (dataWin->strg.count > 5) printf(" ... and %u more\n", dataWin->strg.count - 5);
+ }
+ printf("\n");
+
+ // TXTR
+ printf("-- TXTR (Textures) --\n");
+ printf(" Textures: %u\n", dataWin->txtr.count);
+ if (dataWin->txtr.count > 0) {
+ forEachIndexed(Texture, tex, idx, dataWin->txtr.textures, dataWin->txtr.count) {
+ printf(" [%u] offset=0x%08X size=%u bytes\n", idx, tex->blobOffset, tex->blobSize);
+ }
+ }
+ printf("\n");
+
+ // AUDO
+ printf("-- AUDO (Audio) --\n");
+ printf(" Audio Entries: %u\n", dataWin->audo.count);
+ if (dataWin->audo.count > 0) {
+ uint32_t show = dataWin->audo.count < 3 ? dataWin->audo.count : 3;
+ forEachIndexed(AudioEntry, ae, idx, dataWin->audo.entries, show) {
+ printf(" [%u] offset=0x%08X size=%u bytes\n", idx, ae->dataOffset, ae->dataSize);
+ }
+ if (dataWin->audo.count > 3) printf(" ... and %u more\n", dataWin->audo.count - 3);
+ }
+ printf("\n");
+
+ printf("-- Room Instances --\n");
+ forEach(Room, room, dataWin->room.rooms, dataWin->room.count) {
+ printf("Room %s\n", room->name);
+
+ if (!room->payloadLoaded) {
+ printf(" (payload not loaded)\n");
+ continue;
+ }
+
+ forEachIndexed(RoomGameObject, roomGameObject, idx, room->gameObjects, room->gameObjectCount) {
+ int32_t objectDefinitionId = roomGameObject->objectDefinition;
+ GameObject* objectDefinition = &dataWin->objt.objects[objectDefinitionId];
+ printf(" Object %d (%s, x=%d, y=%d)\n", objectDefinitionId, objectDefinition->name, roomGameObject->x, roomGameObject->y);
+ }
+ }
+
+ // Overall summary
+ printf("===== DataWin parse complete =====\n");
+}
diff --git a/src/file_system.h b/src/file_system.h
index 2341fd7c..64d46dad 100644
--- a/src/file_system.h
+++ b/src/file_system.h
@@ -1,29 +1,29 @@
-#pragma once
-
-#include "common.h"
-#include
-// ===[ FileSystem Vtable ]===
-// Platform-agnostic file system interface
-
-typedef struct FileSystem FileSystem;
-
-typedef struct {
- // Resolve a game-relative path to a full platform path (caller frees result)
- char* (*resolvePath)(FileSystem* fs, const char* relativePath);
- // Check if a file exists
- bool (*fileExists)(FileSystem* fs, const char* relativePath);
- // Read entire file contents into a string (caller frees result), returns nullptr if not found
- char* (*readFileText)(FileSystem* fs, const char* relativePath);
- // Write string contents to a file (creates/overwrites), returns true on success
- bool (*writeFileText)(FileSystem* fs, const char* relativePath, const char* contents);
- // Delete a file, returns true on success
- bool (*deleteFile)(FileSystem* fs, const char* relativePath);
- // Read entire file as binary data (caller frees *outData), returns true on success
- bool (*readFileBinary)(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize);
- // Write binary data to a file (creates/overwrites), returns true on success
- bool (*writeFileBinary)(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size);
-} FileSystemVtable;
-
-struct FileSystem {
- FileSystemVtable* vtable;
-};
+#pragma once
+
+#include "common.h"
+#include
+// ===[ FileSystem Vtable ]===
+// Platform-agnostic file system interface
+
+typedef struct FileSystem FileSystem;
+
+typedef struct {
+ // Resolve a game-relative path to a full platform path (caller frees result)
+ char* (*resolvePath)(FileSystem* fs, const char* relativePath);
+ // Check if a file exists
+ bool (*fileExists)(FileSystem* fs, const char* relativePath);
+ // Read entire file contents into a string (caller frees result), returns nullptr if not found
+ char* (*readFileText)(FileSystem* fs, const char* relativePath);
+ // Write string contents to a file (creates/overwrites), returns true on success
+ bool (*writeFileText)(FileSystem* fs, const char* relativePath, const char* contents);
+ // Delete a file, returns true on success
+ bool (*deleteFile)(FileSystem* fs, const char* relativePath);
+ // Read entire file as binary data (caller frees *outData), returns true on success
+ bool (*readFileBinary)(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize);
+ // Write binary data to a file (creates/overwrites), returns true on success
+ bool (*writeFileBinary)(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size);
+} FileSystemVtable;
+
+struct FileSystem {
+ FileSystemVtable* vtable;
+};
diff --git a/src/gl/gl_renderer.c b/src/gl/gl_renderer.c
deleted file mode 100644
index 5b2515d2..00000000
--- a/src/gl/gl_renderer.c
+++ /dev/null
@@ -1,1467 +0,0 @@
-#include "gl_renderer.h"
-#include "matrix_math.h"
-#include "text_utils.h"
-
-#include
-#include
-#include
-#include
-#include
-
-#include "stb_image.h"
-#include "stb_ds.h"
-#include "utils.h"
-#include "image_decoder.h"
-
-// ===[ Constants ]===
-#define MAX_QUADS 4096
-#define FLOATS_PER_VERTEX 8 // x, y, u, v, r, g, b, a
-#define VERTICES_PER_QUAD 4
-#define INDICES_PER_QUAD 6
-
-// ===[ Shader Sources ]===
-#ifdef ENABLE_GLES
- #define GLSL_VERSION_DIRECTIVE "#version 300 es\n"
- #define GLSL_VERTEX_PRECISION "precision highp float;\n"
- #define GLSL_FRAGMENT_PRECISION "precision mediump float;\n"
-#else
- #define GLSL_VERSION_DIRECTIVE "#version 410 core\n"
- #define GLSL_VERTEX_PRECISION ""
- #define GLSL_FRAGMENT_PRECISION ""
-#endif
-
-static const char* vertexShaderSource =
- GLSL_VERSION_DIRECTIVE
- GLSL_VERTEX_PRECISION
- "layout(location = 0) in vec2 aPos;\n"
- "layout(location = 1) in vec2 aTexCoord;\n"
- "layout(location = 2) in vec4 aColor;\n"
- "uniform mat4 uProjection;\n"
- "out vec2 vTexCoord;\n"
- "out vec4 vColor;\n"
- "void main() {\n"
- " gl_Position = uProjection * vec4(aPos, 0.0, 1.0);\n"
- " vTexCoord = aTexCoord;\n"
- " vColor = aColor;\n"
- "}\n";
-
-static const char* fragmentShaderSource =
- GLSL_VERSION_DIRECTIVE
- GLSL_FRAGMENT_PRECISION
- "in vec2 vTexCoord;\n"
- "in vec4 vColor;\n"
- "uniform sampler2D uTexture;\n"
- "uniform float uAlphaTestRef;\n" // negative = disabled
- "out vec4 fragColor;\n"
- "void main() {\n"
- " vec4 c = texture(uTexture, vTexCoord) * vColor;\n"
- " if (uAlphaTestRef >= c.a) discard;\n"
- " fragColor = c;\n"
- "}\n";
-
-// ===[ Shader Compilation ]===
-
-static GLuint compileShader(GLenum type, const char* source) {
- GLuint shader = glCreateShader(type);
- glShaderSource(shader, 1, &source, nullptr);
- glCompileShader(shader);
-
- GLint success;
- glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
- if (!success) {
- char infoLog[512];
- glGetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog);
- fprintf(stderr, "GL: Shader compilation failed: %s\n", infoLog);
- abort();
- }
- return shader;
-}
-
-static GLuint linkProgram(GLuint vertShader, GLuint fragShader) {
- GLuint program = glCreateProgram();
- glAttachShader(program, vertShader);
- glAttachShader(program, fragShader);
- glLinkProgram(program);
-
- GLint success;
- glGetProgramiv(program, GL_LINK_STATUS, &success);
- if (!success) {
- char infoLog[512];
- glGetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
- fprintf(stderr, "GL: Shader linking failed: %s\n", infoLog);
- abort();
- }
- return program;
-}
-
-// ===[ Batch Flush ]===
-
-static void flushBatch(GLRenderer* gl) {
- if (gl->quadCount == 0) return;
-
- int32_t vertexCount = gl->quadCount * VERTICES_PER_QUAD;
- int32_t indexCount = gl->quadCount * INDICES_PER_QUAD;
-
- // Bind the VAO so the EBO binding it carries is what glDrawElements uses.
- // Without this, glDrawElements would treat the nullptr indices arg as a literal pointer to client memory and SEGV inside the driver during async upload.
- glBindVertexArray(gl->vao);
-
- glBindBuffer(GL_ARRAY_BUFFER, gl->vbo);
- glBufferSubData(GL_ARRAY_BUFFER, 0, vertexCount * FLOATS_PER_VERTEX * sizeof(float), gl->vertexData);
-
- glBindTexture(GL_TEXTURE_2D, gl->currentTextureId);
- glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr);
-
- gl->quadCount = 0;
-}
-
-// ===[ Vtable Implementations ]===
-
-static void glInit(Renderer* renderer, DataWin* dataWin) {
- GLRenderer* gl = (GLRenderer*) renderer;
- renderer->dataWin = dataWin;
-
- // Compile shaders
- GLuint vertShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
- GLuint fragShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
- gl->shaderProgram = linkProgram(vertShader, fragShader);
- glDeleteShader(vertShader);
- glDeleteShader(fragShader);
-
- gl->uProjection = glGetUniformLocation(gl->shaderProgram, "uProjection");
- gl->uTexture = glGetUniformLocation(gl->shaderProgram, "uTexture");
- gl->uAlphaTestRef = glGetUniformLocation(gl->shaderProgram, "uAlphaTestRef");
- gl->alphaTestEnable = false;
- gl->alphaTestRef = 0.0f;
- glUseProgram(gl->shaderProgram);
- glUniform1f(gl->uAlphaTestRef, -1.0f);
-
- // Create VAO/VBO/EBO
- glGenVertexArrays(1, &gl->vao);
- glGenBuffers(1, &gl->vbo);
- glGenBuffers(1, &gl->ebo);
-
- glBindVertexArray(gl->vao);
-
- // VBO: sized for max quads
- int32_t vboSize = MAX_QUADS * VERTICES_PER_QUAD * FLOATS_PER_VERTEX * (int32_t) sizeof(float);
- glBindBuffer(GL_ARRAY_BUFFER, gl->vbo);
- glBufferData(GL_ARRAY_BUFFER, vboSize, nullptr, GL_DYNAMIC_DRAW);
-
- // EBO: pre-fill with quad index pattern (0,1,2,2,3,0 repeated)
- int32_t eboSize = MAX_QUADS * INDICES_PER_QUAD * (int32_t) sizeof(uint32_t);
- uint32_t* indices = safeMalloc(eboSize);
- for (int32_t i = 0; MAX_QUADS > i; i++) {
- uint32_t base = (uint32_t) i * 4;
- indices[i * 6 + 0] = base + 0;
- indices[i * 6 + 1] = base + 1;
- indices[i * 6 + 2] = base + 2;
- indices[i * 6 + 3] = base + 2;
- indices[i * 6 + 4] = base + 3;
- indices[i * 6 + 5] = base + 0;
- }
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl->ebo);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, eboSize, indices, GL_STATIC_DRAW);
- free(indices);
-
- // Vertex attributes: pos(2f), texcoord(2f), color(4f)
- int32_t stride = FLOATS_PER_VERTEX * (int32_t) sizeof(float);
- glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, stride, (void*) 0);
- glEnableVertexAttribArray(0);
- glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*) (2 * sizeof(float)));
- glEnableVertexAttribArray(1);
- glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, stride, (void*) (4 * sizeof(float)));
- glEnableVertexAttribArray(2);
-
- glBindVertexArray(0);
-
- // Allocate CPU-side vertex buffer
- gl->vertexData = safeMalloc(MAX_QUADS * VERTICES_PER_QUAD * FLOATS_PER_VERTEX * sizeof(float));
-
- // Prepare texture slots for lazy loading (PNG decode deferred to first use)
- gl->textureCount = dataWin->txtr.count;
- gl->glTextures = safeMalloc(gl->textureCount * sizeof(GLuint));
- gl->textureWidths = safeMalloc(gl->textureCount * sizeof(int32_t));
- gl->textureHeights = safeMalloc(gl->textureCount * sizeof(int32_t));
- gl->textureLoaded = safeMalloc(gl->textureCount * sizeof(bool));
-
- glGenTextures((GLsizei) gl->textureCount, gl->glTextures);
-
- for (uint32_t i = 0; gl->textureCount > i; i++) {
- gl->textureWidths[i] = 0;
- gl->textureHeights[i] = 0;
- gl->textureLoaded[i] = false;
- }
-
- // Create 1x1 white pixel texture for primitive drawing (rectangles, lines, etc.)
- glGenTextures(1, &gl->whiteTexture);
- glBindTexture(GL_TEXTURE_2D, gl->whiteTexture);
- uint8_t whitePixel[4] = {255, 255, 255, 255};
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-
- // Enable blending
- glEnable(GL_BLEND);
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-
- gl->quadCount = 0;
- gl->currentTextureId = 0;
-
- // Create FBO (texture will be allocated/resized in beginFrame)
- glGenFramebuffers(1, &gl->fbo);
- gl->fboTexture = 0;
- gl->fboWidth = 0;
- gl->fboHeight = 0;
-
- // Save original counts so we know which slots are from data.win vs dynamic
- gl->originalTexturePageCount = gl->textureCount;
- gl->originalTpagCount = dataWin->tpag.count;
- gl->originalSpriteCount = dataWin->sprt.count;
-
- fprintf(stderr, "GL: Renderer initialized (%u texture pages)\n", gl->textureCount);
-}
-
-static void glDestroy(Renderer* renderer) {
- GLRenderer* gl = (GLRenderer*) renderer;
-
- if (gl->fboTexture != 0) glDeleteTextures(1, &gl->fboTexture);
- glDeleteFramebuffers(1, &gl->fbo);
- glDeleteTextures(1, &gl->whiteTexture);
-
- glDeleteTextures((GLsizei) gl->textureCount, gl->glTextures);
- glDeleteProgram(gl->shaderProgram);
- glDeleteVertexArrays(1, &gl->vao);
- glDeleteBuffers(1, &gl->vbo);
- glDeleteBuffers(1, &gl->ebo);
-
- free(gl->glTextures);
- free(gl->textureWidths);
- free(gl->textureHeights);
- free(gl->textureLoaded);
- free(gl->vertexData);
- free(gl);
-}
-
-static void glBeginFrame(Renderer* renderer, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH) {
- GLRenderer* gl = (GLRenderer*) renderer;
-
- gl->quadCount = 0;
- gl->currentTextureId = 0;
- gl->windowW = windowW;
- gl->windowH = windowH;
- gl->gameW = gameW;
- gl->gameH = gameH;
-
- // Resize FBO to game resolution if needed
- if (gameW != gl->fboWidth || gameH != gl->fboHeight) {
- if (gl->fboTexture != 0) glDeleteTextures(1, &gl->fboTexture);
-
- glGenTextures(1, &gl->fboTexture);
- glBindTexture(GL_TEXTURE_2D, gl->fboTexture);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gameW, gameH, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-
- glBindFramebuffer(GL_FRAMEBUFFER, gl->fbo);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl->fboTexture, 0);
-
- GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
- if (status != GL_FRAMEBUFFER_COMPLETE) {
- fprintf(stderr, "GL: Framebuffer incomplete (status=0x%X)\n", status);
- }
-
- gl->fboWidth = gameW;
- gl->fboHeight = gameH;
- fprintf(stderr, "GL: FBO resized to %dx%d\n", gameW, gameH);
- }
-
- // Bind FBO and clear
- glBindFramebuffer(GL_FRAMEBUFFER, gl->fbo);
- glViewport(0, 0, gameW, gameH);
-}
-
-static void glBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle) {
- GLRenderer* gl = (GLRenderer*) renderer;
-
- gl->quadCount = 0;
- gl->currentTextureId = 0;
-
- // Set viewport and scissor to the port rectangle within the FBO
- // FBO uses game resolution, port coordinates are in game space
- // OpenGL viewport Y is bottom-up, game Y is top-down
- int32_t glPortY = gl->gameH - portY - portH;
- glViewport(portX, glPortY, portW, portH);
- glEnable(GL_SCISSOR_TEST);
- glScissor(portX, glPortY, portW, portH);
-
- // Build orthographic projection (Y-down for GML coordinate system)
- Matrix4f projection;
- Matrix4f_identity(&projection);
- Matrix4f_ortho(&projection, (float) viewX, (float) (viewX + viewW), (float) (viewY + viewH), (float) viewY, -1.0f, 1.0f);
-
- if (viewAngle != 0.0f) {
- // GML view_angle: rotate camera by this angle (degrees, counter-clockwise)
- // To rotate the camera, we rotate the world in the opposite direction around the view center
- float cx = (float) viewX + (float) viewW / 2.0f;
- float cy = (float) viewY + (float) viewH / 2.0f;
- Matrix4f rot;
- Matrix4f_identity(&rot);
- Matrix4f_translate(&rot, cx, cy, 0.0f);
- float angleRad = viewAngle * (float) M_PI / 180.0f;
- Matrix4f_rotateZ(&rot, -angleRad);
- Matrix4f_translate(&rot, -cx, -cy, 0.0f);
- Matrix4f result;
- Matrix4f_multiply(&result, &projection, &rot);
- projection = result;
- }
-
- glUseProgram(gl->shaderProgram);
- glUniformMatrix4fv(gl->uProjection, 1, GL_FALSE, projection.m);
- glUniform1i(gl->uTexture, 0);
- glActiveTexture(GL_TEXTURE0);
-
- glBindVertexArray(gl->vao);
-}
-
-static void glEndView(Renderer* renderer) {
- GLRenderer* gl = (GLRenderer*) renderer;
- flushBatch(gl);
- glDisable(GL_SCISSOR_TEST);
-}
-
-static void glBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH) {
- GLRenderer* gl = (GLRenderer*) renderer;
-
- gl->quadCount = 0;
- gl->currentTextureId = 0;
-
- int32_t glPortY = gl->gameH - portY - portH;
- glViewport(portX, glPortY, portW, portH);
- glEnable(GL_SCISSOR_TEST);
- glScissor(portX, glPortY, portW, portH);
-
- Matrix4f projection;
- Matrix4f_identity(&projection);
- Matrix4f_ortho(&projection, 0.0f, (float) guiW, (float) guiH, 0.0f, -1.0f, 1.0f);
-
- glUseProgram(gl->shaderProgram);
- glUniformMatrix4fv(gl->uProjection, 1, GL_FALSE, projection.m);
- glUniform1i(gl->uTexture, 0);
- glActiveTexture(GL_TEXTURE0);
-
- glBindVertexArray(gl->vao);
-}
-
-static void glEndGUI(Renderer* renderer) {
- GLRenderer* gl = (GLRenderer*) renderer;
- flushBatch(gl);
- glDisable(GL_SCISSOR_TEST);
-}
-
-static void glEndFrame(Renderer* renderer) {
- GLRenderer* gl = (GLRenderer*) renderer;
- glBindVertexArray(0);
-
- int effectiveEndX, effectiveEndY;
- int effectiveStartX, effectiveStartY;
-
- // Try and match the "intended" aspect ratio as closely
- // as possible while still fitting on the screen
- if ((gl->gameW * gl->windowH) / gl->gameH < gl->windowW) {
- effectiveEndX = (gl->gameW * gl->windowH) / gl->gameH;
- effectiveEndY = gl->windowH;
- } else {
- effectiveEndX = gl->windowW;
- effectiveEndY = (gl->gameH * gl->windowW) / gl->gameW;
- }
- effectiveStartX = (gl->windowW - effectiveEndX) / 2;
- effectiveStartY = (gl->windowH - effectiveEndY) / 2;
- effectiveEndX += effectiveStartX;
- effectiveEndY += effectiveStartY;
-
- // Blit the full game-resolution FBO to the window
- glBindFramebuffer(GL_READ_FRAMEBUFFER, gl->fbo);
- glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
- glBlitFramebuffer(0, 0, gl->fboWidth, gl->fboHeight, effectiveStartX, effectiveStartY, effectiveEndX, effectiveEndY, GL_COLOR_BUFFER_BIT, GL_NEAREST);
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
-}
-
-static void glRendererFlush(Renderer* renderer) {
- flushBatch((GLRenderer*) renderer);
-}
-
-// Lazily decodes and uploads a TXTR page on first access.
-// Returns true if the texture is ready, false if it failed to decode.
-static bool ensureTextureLoaded(GLRenderer* gl, uint32_t pageId) {
- if (gl->textureLoaded[pageId]) return (gl->textureWidths[pageId] != 0);
-
- gl->textureLoaded[pageId] = true;
-
- DataWin* dw = gl->base.dataWin;
- Texture* txtr = &dw->txtr.textures[pageId];
-
- int w, h;
- bool gm2022_5 = DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0);
- uint8_t* pixels = ImageDecoder_decodeToRgba(txtr->blobData, (size_t) txtr->blobSize, gm2022_5, &w, &h);
- if (pixels == nullptr) {
- fprintf(stderr, "GL: Failed to decode TXTR page %u\n", pageId);
- return false;
- }
-
- gl->textureWidths[pageId] = w;
- gl->textureHeights[pageId] = h;
-
- glBindTexture(GL_TEXTURE_2D, gl->glTextures[pageId]);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-
- free(pixels);
- fprintf(stderr, "GL: Loaded TXTR page %u (%dx%d)\n", pageId, w, h);
- return true;
-}
-
-// Resolves a TPAG index to a loaded GL texture. Returns false if drawing should be skipped.
-static bool resolveSpriteTexture(GLRenderer* gl, int32_t tpagIndex, TexturePageItem** outTpag, GLuint* outTexId, int32_t* outTexW, int32_t* outTexH) {
- DataWin* dw = gl->base.dataWin;
- if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return false;
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- int16_t pageId = tpag->texturePageId;
- if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return false;
- if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return false;
- *outTpag = tpag;
- *outTexId = gl->glTextures[pageId];
- *outTexW = gl->textureWidths[pageId];
- *outTexH = gl->textureHeights[pageId];
- return true;
-}
-
-// Emits a single textured quad into the batch given 4 final screen-space corners (TL, TR, BR, BL), 4 UVs forming a rect (u0,v0)-(u1,v1), and a flat color/alpha.
-// Handles texture rebinding and batch flushing.
-static void emitTexturedQuad(GLRenderer* gl, GLuint texId, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float u0, float v0, float u1, float v1, float r, float g, float b, float alpha) {
- if (gl->quadCount > 0 && gl->currentTextureId != texId) flushBatch(gl);
- if (gl->quadCount >= MAX_QUADS) flushBatch(gl);
- gl->currentTextureId = texId;
-
- float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX;
-
- // Vertex 0: top-left
- verts[0] = x0; verts[1] = y0; verts[2] = u0; verts[3] = v0;
- verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = alpha;
-
- // Vertex 1: top-right
- verts[8] = x1; verts[9] = y1; verts[10] = u1; verts[11] = v0;
- verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = alpha;
-
- // Vertex 2: bottom-right
- verts[16] = x2; verts[17] = y2; verts[18] = u1; verts[19] = v1;
- verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = alpha;
-
- // Vertex 3: bottom-left
- verts[24] = x3; verts[25] = y3; verts[26] = u0; verts[27] = v1;
- verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = alpha;
-
- gl->quadCount++;
-}
-
-static void glDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) {
- GLRenderer* gl = (GLRenderer*) renderer;
- TexturePageItem* tpag;
- GLuint texId;
- int32_t texW, texH;
- if (!resolveSpriteTexture(gl, tpagIndex, &tpag, &texId, &texW, &texH)) return;
-
- // Compute normalized UVs from TPAG source rect
- float u0 = (float) tpag->sourceX / (float) texW;
- float v0 = (float) tpag->sourceY / (float) texH;
- float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW;
- float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH;
-
- // Compute local quad corners (relative to origin, with target offset)
- float localX0 = (float) tpag->targetX - originX;
- float localY0 = (float) tpag->targetY - originY;
- float localX1 = localX0 + (float) tpag->sourceWidth;
- float localY1 = localY0 + (float) tpag->sourceHeight;
-
- // Build 2D transform: T(x,y) * R(-angleDeg) * S(xscale, yscale)
- // GML rotation is counter-clockwise, OpenGL rotation is counter-clockwise, but
- // since we have Y-down, we negate the angle to get the correct visual rotation
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- Matrix4f transform;
- Matrix4f_setTransform2D(&transform, x, y, xscale, yscale, angleRad);
-
- // Transform 4 corners
- float x0, y0, x1, y1, x2, y2, x3, y3;
- Matrix4f_transformPoint(&transform, localX0, localY0, &x0, &y0); // top-left
- Matrix4f_transformPoint(&transform, localX1, localY0, &x1, &y1); // top-right
- Matrix4f_transformPoint(&transform, localX1, localY1, &x2, &y2); // bottom-right
- Matrix4f_transformPoint(&transform, localX0, localY1, &x3, &y3); // bottom-left
-
- // Convert BGR color to RGB floats
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- emitTexturedQuad(gl, texId, x0, y0, x1, y1, x2, y2, x3, y3, u0, v0, u1, v1, r, g, b, alpha);
-}
-
-static void glDrawSpritePart(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) {
- GLRenderer* gl = (GLRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return;
-
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- int16_t pageId = tpag->texturePageId;
- if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return;
- if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return;
-
- GLuint texId = gl->glTextures[pageId];
- int32_t texW = gl->textureWidths[pageId];
- int32_t texH = gl->textureHeights[pageId];
-
- // Compute UVs for the sub-region within the atlas
- float u0 = (float) (tpag->sourceX + srcOffX) / (float) texW;
- float v0 = (float) (tpag->sourceY + srcOffY) / (float) texH;
- float u1 = (float) (tpag->sourceX + srcOffX + srcW) / (float) texW;
- float v1 = (float) (tpag->sourceY + srcOffY + srcH) / (float) texH;
-
- // Convert BGR color to RGB floats
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- // Quad corners (no origin offset - draw_sprite_part ignores sprite origin)
- float cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3;
- if (angleDeg == 0.0f) {
- cx0 = x; cy0 = y;
- cx1 = x + (float) srcW * xscale; cy1 = y;
- cx2 = x + (float) srcW * xscale; cy2 = y + (float) srcH * yscale;
- cx3 = x; cy3 = y + (float) srcH * yscale;
- } else {
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- float cosA = cosf(angleRad);
- float sinA = sinf(angleRad);
- float qx0 = x, qy0 = y;
- float qx1 = x + (float) srcW * xscale, qy1 = y;
- float qx2 = x + (float) srcW * xscale, qy2 = y + (float) srcH * yscale;
- float qx3 = x, qy3 = y + (float) srcH * yscale;
- float dx, dy;
- dx = qx0 - pivotX; dy = qy0 - pivotY; cx0 = cosA * dx - sinA * dy + pivotX; cy0 = sinA * dx + cosA * dy + pivotY;
- dx = qx1 - pivotX; dy = qy1 - pivotY; cx1 = cosA * dx - sinA * dy + pivotX; cy1 = sinA * dx + cosA * dy + pivotY;
- dx = qx2 - pivotX; dy = qy2 - pivotY; cx2 = cosA * dx - sinA * dy + pivotX; cy2 = sinA * dx + cosA * dy + pivotY;
- dx = qx3 - pivotX; dy = qy3 - pivotY; cx3 = cosA * dx - sinA * dy + pivotX; cy3 = sinA * dx + cosA * dy + pivotY;
- }
-
- emitTexturedQuad(gl, texId, cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3, u0, v0, u1, v1, r, g, b, alpha);
-}
-
-static void glDrawSpritePos(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) {
- GLRenderer* gl = (GLRenderer*) renderer;
- TexturePageItem* tpag;
- GLuint texId;
- int32_t texW, texH;
- if (!resolveSpriteTexture(gl, tpagIndex, &tpag, &texId, &texW, &texH)) return;
-
- float u0 = (float) tpag->sourceX / (float) texW;
- float v0 = (float) tpag->sourceY / (float) texH;
- float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW;
- float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH;
-
- emitTexturedQuad(gl, texId, x1, y1, x2, y2, x3, y3, x4, y4, u0, v0, u1, v1, 1.0f, 1.0f, 1.0f, alpha);
-}
-
-// Emits a single colored quad into the batch using the white pixel texture
-static void emitColoredQuad(GLRenderer* gl, float x0, float y0, float x1, float y1, float r, float g, float b, float a) {
- // Flush if texture changed or batch full
- if (gl->quadCount > 0 && gl->currentTextureId != gl->whiteTexture) flushBatch(gl);
- if (gl->quadCount >= MAX_QUADS) flushBatch(gl);
-
- gl->currentTextureId = gl->whiteTexture;
-
- float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX;
-
- // All UVs point to (0.5, 0.5) center of the 1x1 white texture
- // Vertex 0: top-left
- verts[0] = x0; verts[1] = y0; verts[2] = 0.5f; verts[3] = 0.5f;
- verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = a;
-
- // Vertex 1: top-right
- verts[8] = x1; verts[9] = y0; verts[10] = 0.5f; verts[11] = 0.5f;
- verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = a;
-
- // Vertex 2: bottom-right
- verts[16] = x1; verts[17] = y1; verts[18] = 0.5f; verts[19] = 0.5f;
- verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = a;
-
- // Vertex 3: bottom-left
- verts[24] = x0; verts[25] = y1; verts[26] = 0.5f; verts[27] = 0.5f;
- verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = a;
-
- gl->quadCount++;
-}
-
-static void glDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) {
- GLRenderer* gl = (GLRenderer*) renderer;
-
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- if (outline) {
- // Draw 4 one-pixel-wide edges: top, bottom, left, right
- emitColoredQuad(gl, x1, y1, x2 + 1, y1 + 1, r, g, b, alpha); // top
- emitColoredQuad(gl, x1, y2, x2 + 1, y2 + 1, r, g, b, alpha); // bottom
- emitColoredQuad(gl, x1, y1 + 1, x1 + 1, y2, r, g, b, alpha); // left
- emitColoredQuad(gl, x2, y1 + 1, x2 + 1, y2, r, g, b, alpha); // right
- } else {
- // Filled rectangle: GML adds +1 to width/height for filled rects
- emitColoredQuad(gl, x1, y1, x2 + 1, y2 + 1, r, g, b, alpha);
- }
-}
-
-// ===[ Line Drawing ]===
-
-static void glDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha) {
- GLRenderer* gl = (GLRenderer*) renderer;
-
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- // Compute perpendicular offset for line thickness
- float dx = x2 - x1;
- float dy = y2 - y1;
- float len = sqrtf(dx * dx + dy * dy);
- if (0.0001f > len) return;
-
- float halfW = width * 0.5f;
- float px = (-dy / len) * halfW;
- float py = (dx / len) * halfW;
-
- // Emit quad as 4 vertices forming a rectangle along the line
- if (gl->quadCount > 0 && gl->currentTextureId != gl->whiteTexture) {
- flushBatch(gl);
- }
- if (gl->quadCount >= MAX_QUADS) {
- flushBatch(gl);
- }
- gl->currentTextureId = gl->whiteTexture;
-
- float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX;
-
- // Vertex 0: start + perpendicular
- verts[0] = x1 + px; verts[1] = y1 + py; verts[2] = 0.5f; verts[3] = 0.5f;
- verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = alpha;
-
- // Vertex 1: start - perpendicular
- verts[8] = x1 - px; verts[9] = y1 - py; verts[10] = 0.5f; verts[11] = 0.5f;
- verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = alpha;
-
- // Vertex 2: end - perpendicular
- verts[16] = x2 - px; verts[17] = y2 - py; verts[18] = 0.5f; verts[19] = 0.5f;
- verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = alpha;
-
- // Vertex 3: end + perpendicular
- verts[24] = x2 + px; verts[25] = y2 + py; verts[26] = 0.5f; verts[27] = 0.5f;
- verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = alpha;
-
- gl->quadCount++;
-}
-
-static void glDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha) {
- GLRenderer* gl = (GLRenderer*) renderer;
-
- float r1 = (float) BGR_R(color1) / 255.0f;
- float g1 = (float) BGR_G(color1) / 255.0f;
- float b1 = (float) BGR_B(color1) / 255.0f;
-
- float r2 = (float) BGR_R(color2) / 255.0f;
- float g2 = (float) BGR_G(color2) / 255.0f;
- float b2 = (float) BGR_B(color2) / 255.0f;
-
- // Compute perpendicular offset for line thickness
- float dx = x2 - x1;
- float dy = y2 - y1;
- float len = sqrtf(dx * dx + dy * dy);
- if (0.0001f > len) return;
-
- float halfW = width * 0.5f;
- float px = (-dy / len) * halfW;
- float py = (dx / len) * halfW;
-
- // Emit quad with per-vertex colors (color1 at start, color2 at end)
- if (gl->quadCount > 0 && gl->currentTextureId != gl->whiteTexture) {
- flushBatch(gl);
- }
- if (gl->quadCount >= MAX_QUADS) {
- flushBatch(gl);
- }
- gl->currentTextureId = gl->whiteTexture;
-
- float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX;
-
- // Vertex 0: start + perpendicular (color1)
- verts[0] = x1 + px; verts[1] = y1 + py; verts[2] = 0.5f; verts[3] = 0.5f;
- verts[4] = r1; verts[5] = g1; verts[6] = b1; verts[7] = alpha;
-
- // Vertex 1: start - perpendicular (color1)
- verts[8] = x1 - px; verts[9] = y1 - py; verts[10] = 0.5f; verts[11] = 0.5f;
- verts[12] = r1; verts[13] = g1; verts[14] = b1; verts[15] = alpha;
-
- // Vertex 2: end - perpendicular (color2)
- verts[16] = x2 - px; verts[17] = y2 - py; verts[18] = 0.5f; verts[19] = 0.5f;
- verts[20] = r2; verts[21] = g2; verts[22] = b2; verts[23] = alpha;
-
- // Vertex 3: end + perpendicular (color2)
- verts[24] = x2 + px; verts[25] = y2 + py; verts[26] = 0.5f; verts[27] = 0.5f;
- verts[28] = r2; verts[29] = g2; verts[30] = b2; verts[31] = alpha;
-
- gl->quadCount++;
-}
-
-static void glDrawTriangle(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline)
-{
- GLRenderer* gl = (GLRenderer*) renderer;
- if(outline)
- {
- glDrawLine(renderer, x1, y1, x2, y2, 1, renderer->drawColor, 1.0);
- glDrawLine(renderer, x2, y2, x3, y3, 1, renderer->drawColor, 1.0);
- glDrawLine(renderer, x3, y3, x1, y1, 1, renderer->drawColor, 1.0);
- } else {
- float r = (float) BGR_R(renderer->drawColor) / 255.0f;
- float g = (float) BGR_G(renderer->drawColor) / 255.0f;
- float b = (float) BGR_B(renderer->drawColor) / 255.0f;
-
- flushBatch(gl);
-
- int i = 0;
- float verts[24] = {
- x1, y1, 0.0f, 0.0f, r, g, b, renderer->drawAlpha,
- x2, y2, 0.0f, 0.0f, r, g, b, renderer->drawAlpha,
- x3, y3, 0.0f, 0.0f, r, g, b, renderer->drawAlpha,
- };
-
- glBindBuffer(GL_ARRAY_BUFFER, gl->vbo);
- glBufferSubData(GL_ARRAY_BUFFER, 0, 3 * FLOATS_PER_VERTEX * sizeof(float), verts);
-
- glBindTexture(GL_TEXTURE_2D, gl->whiteTexture);
- glDrawArrays(GL_TRIANGLES, 0, 3);
- }
-}
-
-// ===[ Text Drawing ]===
-
-// Resolved font state shared between glDrawText and glDrawTextColor
-typedef struct {
- Font* font;
- TexturePageItem* fontTpag; // single TPAG for regular fonts (nullptr for sprite fonts)
- GLuint texId;
- int32_t texW, texH;
- Sprite* spriteFontSprite; // source sprite for sprite fonts (nullptr for regular fonts)
-} GlFontState;
-
-// Resolves font texture state
-// Returns false if the font can't be drawn
-static bool glResolveFontState(GLRenderer* gl, DataWin* dw, Font* font, GlFontState* state) {
- state->font = font;
- state->fontTpag = nullptr;
- state->texId = 0;
- state->texW = 0;
- state->texH = 0;
- state->spriteFontSprite = nullptr;
-
- if (!font->isSpriteFont) {
- int32_t fontTpagIndex = font->tpagIndex;
- if (0 > fontTpagIndex) return false;
-
- state->fontTpag = &dw->tpag.items[fontTpagIndex];
- int16_t pageId = state->fontTpag->texturePageId;
- if (0 > pageId || (uint32_t) pageId >= gl->textureCount) return false;
- if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return false;
-
- state->texId = gl->glTextures[pageId];
- state->texW = gl->textureWidths[pageId];
- state->texH = gl->textureHeights[pageId];
- } else if (font->spriteIndex >= 0 && dw->sprt.count > (uint32_t) font->spriteIndex) {
- state->spriteFontSprite = &dw->sprt.sprites[font->spriteIndex];
- }
- return true;
-}
-
-// Resolves UV coordinates, texture ID, and local position for a single glyph
-// Returns false if the glyph can't be drawn
-static bool glResolveGlyph(GLRenderer* gl, DataWin* dw, GlFontState* state, FontGlyph* glyph, float cursorX, float cursorY, GLuint* outTexId, float* outU0, float* outV0, float* outU1, float* outV1, float* outLocalX0, float* outLocalY0) {
- Font* font = state->font;
- if (font->isSpriteFont && state->spriteFontSprite != nullptr) {
- Sprite* sprite = state->spriteFontSprite;
- int32_t glyphIndex = (int32_t) (glyph - font->glyphs);
- if (0 > glyphIndex || glyphIndex >= (int32_t) sprite->textureCount) return false;
-
- int32_t tpagIdx = sprite->tpagIndices[glyphIndex];
- if (0 > tpagIdx) return false;
-
- TexturePageItem* glyphTpag = &dw->tpag.items[tpagIdx];
- int16_t pid = glyphTpag->texturePageId;
- if (0 > pid || (uint32_t) pid >= gl->textureCount) return false;
- if (!ensureTextureLoaded(gl, (uint32_t) pid)) return false;
-
- *outTexId = gl->glTextures[pid];
- int32_t tw = gl->textureWidths[pid];
- int32_t th = gl->textureHeights[pid];
-
- *outU0 = (float) glyphTpag->sourceX / (float) tw;
- *outV0 = (float) glyphTpag->sourceY / (float) th;
- *outU1 = (float) (glyphTpag->sourceX + glyphTpag->sourceWidth) / (float) tw;
- *outV1 = (float) (glyphTpag->sourceY + glyphTpag->sourceHeight) / (float) th;
-
- *outLocalX0 = cursorX + (float) glyph->offset;
- *outLocalY0 = cursorY + (float) ((int32_t) glyphTpag->targetY - sprite->originY);
- } else {
- *outTexId = state->texId;
- *outU0 = (float) (state->fontTpag->sourceX + glyph->sourceX) / (float) state->texW;
- *outV0 = (float) (state->fontTpag->sourceY + glyph->sourceY) / (float) state->texH;
- *outU1 = (float) (state->fontTpag->sourceX + glyph->sourceX + glyph->sourceWidth) / (float) state->texW;
- *outV1 = (float) (state->fontTpag->sourceY + glyph->sourceY + glyph->sourceHeight) / (float) state->texH;
-
- *outLocalX0 = cursorX + glyph->offset;
- *outLocalY0 = cursorY;
- }
- return true;
-}
-
-static void glDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg) {
- GLRenderer* gl = (GLRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- int32_t fontIndex = renderer->drawFont;
- if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return;
-
- Font* font = &dw->font.fonts[fontIndex];
-
- GlFontState fontState;
- if (!glResolveFontState(gl, dw, font, &fontState)) return;
-
- uint32_t color = renderer->drawColor;
- float alpha = renderer->drawAlpha;
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- int32_t textLen = (int32_t) strlen(text);
-
- // Count lines, treating \r\n and \n\r as single breaks
- int32_t lineCount = TextUtils_countLines(text, textLen);
-
- // Per-line vertical stride. HTML5 runner's default `linesep` is `max_glyph_height * scaleY`.
- // We apply scaleY via the transform matrix below, so keep the stride in pre-scale (local) coords.
- float lineStride = TextUtils_lineStride(font);
-
- // Vertical alignment offset
- float totalHeight = (float) lineCount * lineStride;
- float valignOffset = 0;
- if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f;
- else if (renderer->drawValign == 2) valignOffset = -totalHeight;
-
- // Build transform matrix
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- Matrix4f transform;
- Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad);
-
- // Iterate through lines. HTML5 subtracts ascenderOffset from the per-line y offset
- // (see yyFont.GR_Text_Draw), shifting glyphs up so the baseline aligns with the drawn y.
- float cursorY = valignOffset - (float) font->ascenderOffset;
- int32_t lineStart = 0;
-
- for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) {
- // Find end of current line
- int32_t lineEnd = lineStart;
- while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) {
- lineEnd++;
- }
- int32_t lineLen = lineEnd - lineStart;
-
- // Horizontal alignment offset for this line
- float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen);
- float halignOffset = 0;
- if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f;
- else if (renderer->drawHalign == 2) halignOffset = -lineWidth;
-
- float cursorX = halignOffset;
-
- // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning)
- int32_t pos = 0;
- uint16_t ch = 0;
- bool hasCh = false;
- if (lineLen > pos) {
- ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
- hasCh = true;
- }
-
- while (hasCh) {
- FontGlyph* glyph = TextUtils_findGlyph(font, ch);
-
- uint16_t nextCh = 0;
- bool hasNext = lineLen > pos;
- if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
-
- if (glyph != nullptr) {
- bool drewSuccessfully = false;
- if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) {
- float u0, v0, u1, v1;
- float localX0, localY0;
- GLuint glyphTexId;
-
- if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) {
- // Flush if texture changed or batch full
- if (gl->quadCount > 0 && gl->currentTextureId != glyphTexId) flushBatch(gl);
- if (gl->quadCount >= MAX_QUADS) flushBatch(gl);
- gl->currentTextureId = glyphTexId;
-
- float localX1 = localX0 + (float) glyph->sourceWidth;
- float localY1 = localY0 + (float) glyph->sourceHeight;
-
- // Transform corners
- float px0, py0, px1, py1, px2, py2, px3, py3;
- Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0);
- Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1);
- Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2);
- Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3);
-
- // Write 4 vertices
- float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX;
-
- verts[0] = px0; verts[1] = py0; verts[2] = u0; verts[3] = v0;
- verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = alpha;
-
- verts[8] = px1; verts[9] = py1; verts[10] = u1; verts[11] = v0;
- verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = alpha;
-
- verts[16] = px2; verts[17] = py2; verts[18] = u1; verts[19] = v1;
- verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = alpha;
-
- verts[24] = px3; verts[25] = py3; verts[26] = u0; verts[27] = v1;
- verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = alpha;
-
- gl->quadCount++;
- drewSuccessfully = true;
- }
- }
-
- cursorX += glyph->shift;
- if (drewSuccessfully && hasNext) {
- cursorX += TextUtils_getKerningOffset(glyph, nextCh);
- }
- }
-
- ch = nextCh;
- hasCh = hasNext;
- }
-
- cursorY += lineStride;
- // Skip past the newline, treating \r\n and \n\r as single breaks
- if (textLen > lineEnd) {
- lineStart = TextUtils_skipNewline(text, lineEnd, textLen);
- } else {
- lineStart = lineEnd;
- }
- }
-}
-
-static void glDrawTextColor(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t _c1, int32_t _c2, int32_t _c3, int32_t _c4, float alpha) {
- GLRenderer* gl = (GLRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- int32_t fontIndex = renderer->drawFont;
- if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return;
-
- Font* font = &dw->font.fonts[fontIndex];
-
- GlFontState fontState;
- if (!glResolveFontState(gl, dw, font, &fontState)) return;
-
- int32_t textLen = (int32_t) strlen(text);
- if(textLen == 0) return;
-
- // Count lines, treating \r\n and \n\r as single breaks
- int32_t lineCount = TextUtils_countLines(text, textLen);
-
- float lineStride = TextUtils_lineStride(font);
-
- // Vertical alignment offset
- float totalHeight = (float) lineCount * lineStride;
- float valignOffset = 0;
- if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f;
- else if (renderer->drawValign == 2) valignOffset = -totalHeight;
-
- // Build transform matrix
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- Matrix4f transform;
- Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad);
-
- // Iterate through lines. HTML5 subtracts ascenderOffset from per-line y offset.
- float cursorY = valignOffset - (float) font->ascenderOffset;
- int32_t lineStart = 0;
-
- // get delta's (16.16 format)
- int32_t left_r_dx = ((_c2 & 0xff0000) - (_c1 & 0xff0000)) / textLen;
- int32_t left_g_dx = ((((_c2 & 0xff00) << 8) - ((_c1 & 0xff00) << 8))) / textLen;
- int32_t left_b_dx = ((((_c2 & 0xff) << 16) - ((_c1 & 0xff) << 16))) / textLen;
-
- int32_t right_r_dx = ((_c3 & 0xff0000) - (_c4 & 0xff0000)) / textLen;
- int32_t right_g_dx = ((((_c3 & 0xff00) << 8) - ((_c4 & 0xff00) << 8))) / textLen;
- int32_t right_b_dx = ((((_c3 & 0xff) << 16) - ((_c4 & 0xff) << 16))) / textLen;
-
- int32_t left_delta_r = left_r_dx;
- int32_t left_delta_g = left_g_dx;
- int32_t left_delta_b = left_b_dx;
- int32_t right_delta_r = right_r_dx;
- int32_t right_delta_g = right_g_dx;
- int32_t right_delta_b = right_b_dx;
-
- int32_t c1 = _c1;
- int32_t c4 = _c4;
-
- for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) {
- // Find end of current line
- int32_t lineEnd = lineStart;
- while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) {
- lineEnd++;
- }
- int32_t lineLen = lineEnd - lineStart;
-
- // Horizontal alignment offset for this line
- float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen);
- float halignOffset = 0;
- if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f;
- else if (renderer->drawHalign == 2) halignOffset = -lineWidth;
-
- float cursorX = halignOffset;
-
- // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning)
- int32_t pos = 0;
- uint16_t ch = 0;
- bool hasCh = false;
- if (lineLen > pos) {
- ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
- hasCh = true;
- }
-
- while (hasCh) {
- // do 16.16 maths
- int32_t c2 = ((c1 & 0xff0000) + (left_delta_r & 0xff0000)) & 0xff0000;
- c2 |= ((c1 & 0xff00) + (left_delta_g >> 8) & 0xff00) & 0xff00;
- c2 |= ((c1 & 0xff) + (left_delta_b >> 16)) & 0xff;
- int32_t c3 = ((c4 & 0xff0000) + (right_delta_r & 0xff0000)) & 0xff0000;
- c3 |= ((c4 & 0xff00) + (right_delta_g >> 8) & 0xff00) & 0xff00;
- c3 |= ((c4 & 0xff) + (right_delta_b >> 16)) & 0xff;
-
- left_delta_r += left_r_dx;
- left_delta_g += left_g_dx;
- left_delta_b += left_b_dx;
- right_delta_r += right_r_dx;
- right_delta_g += right_g_dx;
- right_delta_b += right_b_dx;
-
- FontGlyph* glyph = TextUtils_findGlyph(font, ch);
-
- uint16_t nextCh = 0;
- bool hasNext = lineLen > pos;
- if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
-
- if (glyph != nullptr) {
- bool drewSuccessfully = false;
- if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) {
- float u0, v0, u1, v1;
- float localX0, localY0;
- GLuint glyphTexId;
-
- if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) {
- // Flush if texture changed or batch full
- if (gl->quadCount > 0 && gl->currentTextureId != glyphTexId) flushBatch(gl);
- if (gl->quadCount >= MAX_QUADS) flushBatch(gl);
- gl->currentTextureId = glyphTexId;
-
- float localX1 = localX0 + (float) glyph->sourceWidth;
- float localY1 = localY0 + (float) glyph->sourceHeight;
-
- // Transform corners
- float px0, py0, px1, py1, px2, py2, px3, py3;
- Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0);
- Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1);
- Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2);
- Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3);
-
- // Write 4 vertices
- float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX;
-
- // top left
- verts[0] = px0; verts[1] = py0; verts[2] = u0; verts[3] = v0;
- verts[4] = ((float) BGR_R(c1) / 255.0f); verts[5] = ((float) BGR_G(c1) / 255.0f); verts[6] = ((float) BGR_B(c1) / 255.0f); verts[7] = alpha;
-
- // top right
- verts[8] = px1; verts[9] = py1; verts[10] = u1; verts[11] = v0;
- verts[12] = ((float) BGR_R(c2) / 255.0f); verts[13] = ((float) BGR_G(c2) / 255.0f); verts[14] = ((float) BGR_B(c2) / 255.0f); verts[15] = alpha;
-
- // bottom right
- verts[16] = px2; verts[17] = py2; verts[18] = u1; verts[19] = v1;
- verts[20] = ((float) BGR_R(c3) / 255.0f); verts[21] = ((float) BGR_G(c3) / 255.0f); verts[22] = ((float) BGR_B(c3) / 255.0f); verts[23] = alpha;
-
- // bottom left
- verts[24] = px3; verts[25] = py3; verts[26] = u0; verts[27] = v1;
- verts[28] = ((float) BGR_R(c4) / 255.0f); verts[29] = ((float) BGR_G(c4) / 255.0f); verts[30] = ((float) BGR_B(c4) / 255.0f); verts[31] = alpha;
-
- gl->quadCount++;
- drewSuccessfully = true;
- }
- }
-
- cursorX += glyph->shift;
- if (drewSuccessfully) {
- if (hasNext) cursorX += TextUtils_getKerningOffset(glyph, nextCh);
- c4 = c3; // set left edge to be what the last right edge was....
- c1 = c2;
- }
- }
-
- ch = nextCh;
- hasCh = hasNext;
- }
-
- cursorY += lineStride;
- // Skip past the newline, treating \r\n and \n\r as single breaks
- if (textLen > lineEnd) {
- lineStart = TextUtils_skipNewline(text, lineEnd, textLen);
- } else {
- lineStart = lineEnd;
- }
- }
-}
-
-// ===[ Dynamic Sprite Creation/Deletion ]===
-
-// Finds a free dynamic texture page slot (glTextures[i] == 0), or appends a new one.
-static uint32_t findOrAllocTexturePageSlot(GLRenderer* gl) {
- // Scan dynamic range for a reusable slot
- for (uint32_t i = gl->originalTexturePageCount; gl->textureCount > i; i++) {
- if (gl->glTextures[i] == 0) return i;
- }
- // No free slot found, grow the arrays
- uint32_t newPageId = gl->textureCount;
- gl->textureCount++;
- gl->glTextures = safeRealloc(gl->glTextures, gl->textureCount * sizeof(GLuint));
- gl->textureWidths = safeRealloc(gl->textureWidths, gl->textureCount * sizeof(int32_t));
- gl->textureHeights = safeRealloc(gl->textureHeights, gl->textureCount * sizeof(int32_t));
- gl->textureLoaded = safeRealloc(gl->textureLoaded, gl->textureCount * sizeof(bool));
- gl->glTextures[newPageId] = 0;
- gl->textureWidths[newPageId] = 0;
- gl->textureHeights[newPageId] = 0;
- gl->textureLoaded[newPageId] = false;
- return newPageId;
-}
-
-// Finds a free dynamic TPAG slot (texturePageId == -1), or appends a new one.
-static uint32_t findOrAllocTpagSlot(DataWin* dw, uint32_t originalTpagCount) {
- for (uint32_t i = originalTpagCount; dw->tpag.count > i; i++) {
- if (dw->tpag.items[i].texturePageId == -1) return i;
- }
- uint32_t newIndex = dw->tpag.count;
- dw->tpag.count++;
- dw->tpag.items = safeRealloc(dw->tpag.items, dw->tpag.count * sizeof(TexturePageItem));
- memset(&dw->tpag.items[newIndex], 0, sizeof(TexturePageItem));
- dw->tpag.items[newIndex].texturePageId = -1;
- return newIndex;
-}
-
-static int32_t glCreateSpriteFromSurface(Renderer* renderer, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig) {
- GLRenderer* gl = (GLRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 >= w || 0 >= h) return -1;
-
- // Flush any pending draws before reading pixels
- flushBatch(gl);
-
- // Read pixels from the FBO (application_surface)
- glBindFramebuffer(GL_READ_FRAMEBUFFER, gl->fbo);
-
- uint8_t* pixels = safeMalloc((size_t) w * (size_t) h * 4);
- if (pixels == nullptr) return -1;
-
- // OpenGL Y is bottom-up, GML Y is top-down, so flip the Y coordinate
- int32_t glY = gl->fboHeight - y - h;
- glReadPixels(x, glY, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
-
- // Flip vertically (OpenGL reads bottom-to-top)
- size_t rowBytes = (size_t) w * 4;
- uint8_t* rowTemp = safeMalloc(rowBytes);
- repeat(h / 2, row) {
- uint8_t* top = pixels + row * rowBytes;
- uint8_t* bot = pixels + (h - 1 - row) * rowBytes;
- memcpy(rowTemp, top, rowBytes);
- memcpy(top, bot, rowBytes);
- memcpy(bot, rowTemp, rowBytes);
- }
- free(rowTemp);
-
- // Create a new GL texture from the captured pixels
- GLuint newTexId;
- glGenTextures(1, &newTexId);
- glBindTexture(GL_TEXTURE_2D, newTexId);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, smooth ? GL_LINEAR : GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, smooth ? GL_LINEAR : GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-
- free(pixels);
-
- // Find or allocate slots for texture page, TPAG, and sprite
- uint32_t pageId = findOrAllocTexturePageSlot(gl);
- gl->glTextures[pageId] = newTexId;
- gl->textureWidths[pageId] = w;
- gl->textureHeights[pageId] = h;
- gl->textureLoaded[pageId] = true;
-
- uint32_t tpagIndex = findOrAllocTpagSlot(dw, gl->originalTpagCount);
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- tpag->sourceX = 0;
- tpag->sourceY = 0;
- tpag->sourceWidth = (uint16_t) w;
- tpag->sourceHeight = (uint16_t) h;
- tpag->targetX = 0;
- tpag->targetY = 0;
- tpag->targetWidth = (uint16_t) w;
- tpag->targetHeight = (uint16_t) h;
- tpag->boundingWidth = (uint16_t) w;
- tpag->boundingHeight = (uint16_t) h;
- tpag->texturePageId = (int16_t) pageId;
-
- uint32_t spriteIndex = DataWin_allocSpriteSlot(dw, gl->originalSpriteCount);
- Sprite* sprite = &dw->sprt.sprites[spriteIndex];
- // name was set by DataWin_allocSpriteSlot ("__newsprite"); don't overwrite it here
- sprite->width = (uint32_t) w;
- sprite->height = (uint32_t) h;
- sprite->originX = xorig;
- sprite->originY = yorig;
- sprite->textureCount = 1;
- sprite->tpagIndices = safeMalloc(sizeof(int32_t));
- sprite->tpagIndices[0] = (int32_t) tpagIndex;
- sprite->maskCount = 0;
- sprite->masks = nullptr;
-
- fprintf(stderr, "GL: Created dynamic sprite %u (%dx%d) from surface at (%d,%d)\n", spriteIndex, w, h, x, y);
- return (int32_t) spriteIndex;
-}
-
-static void glDeleteSprite(Renderer* renderer, int32_t spriteIndex) {
- GLRenderer* gl = (GLRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > spriteIndex || dw->sprt.count <= (uint32_t) spriteIndex) return;
-
- // Refuse to delete original data.win sprites
- if (gl->originalSpriteCount > (uint32_t) spriteIndex) {
- fprintf(stderr, "GL: Cannot delete data.win sprite %d\n", spriteIndex);
- return;
- }
-
- Sprite* sprite = &dw->sprt.sprites[spriteIndex];
- if (sprite->textureCount == 0) return; // already deleted
-
- // Clean up GL texture and TPAG entries owned by this sprite.
- // Slots with index >= originalTpagCount are dynamically allocated and ours to free.
- repeat(sprite->textureCount, i) {
- int32_t tpagIdx = sprite->tpagIndices[i];
- if (tpagIdx >= 0 && (uint32_t) tpagIdx >= gl->originalTpagCount) {
- TexturePageItem* tpag = &dw->tpag.items[tpagIdx];
- int16_t pageId = tpag->texturePageId;
- if (pageId >= 0 && gl->textureCount > (uint32_t) pageId) {
- glDeleteTextures(1, &gl->glTextures[pageId]);
- gl->glTextures[pageId] = 0;
- }
- // Mark TPAG slot as free for reuse
- tpag->texturePageId = -1;
- }
- }
-
- // Clear the sprite entry so it won't be drawn and can be reused. Preserve `name` across the memset: the slot is still in sprt.count and must keep a valid string for asset_get_index / name lookups.
- free(sprite->tpagIndices);
- const char* keepName = sprite->name;
- memset(sprite, 0, sizeof(Sprite));
- sprite->name = keepName;
-
- fprintf(stderr, "GL: Deleted sprite %d\n", spriteIndex);
-}
-
-static GLenum gmsBlendModeToGL(int mode) {
- switch(mode) {
- case bm_zero: return GL_ZERO;
- case bm_one: return GL_ONE;
- case bm_src_color: return GL_SRC_COLOR;
- case bm_inv_src_color: return GL_ONE_MINUS_SRC_COLOR;
- case bm_src_alpha: return GL_SRC_ALPHA;
- case bm_inv_src_alpha: return GL_ONE_MINUS_SRC_ALPHA;
- case bm_dest_alpha: return GL_DST_ALPHA;
- case bm_inv_dest_alpha: return GL_ONE_MINUS_DST_ALPHA;
- case bm_dest_color: return GL_DST_COLOR;
- case bm_inv_dest_color: return GL_ONE_MINUS_DST_COLOR;
- case bm_src_alpha_sat: return GL_SRC_ALPHA_SATURATE;
- }
- return GL_ONE;
-}
-
-static GLenum gmsBlendModeToGLEquation(int mode) {
- switch (mode) {
- case bm_normal:
- return GL_FUNC_ADD;
- case bm_add:
- return GL_FUNC_ADD;
- case bm_subtract:
- return GL_FUNC_ADD;
- case bm_reverse_subtract:
- return GL_FUNC_REVERSE_SUBTRACT;
- case bm_min:
- return GL_MIN;
- case bm_max:
- return GL_FUNC_ADD;
- default:
- return GL_FUNC_ADD;
- }
-}
-
-static GLenum gmsBlendModeToGLSFactor(int mode) {
- switch (mode) {
- case bm_normal:
- return GL_SRC_ALPHA;
- case bm_add:
- return GL_SRC_ALPHA;
- case bm_subtract:
- return GL_ZERO;
- case bm_reverse_subtract:
- return GL_SRC_ALPHA;
- case bm_min:
- return GL_ONE;
- case bm_max:
- return GL_SRC_ALPHA;
- default:
- return gmsBlendModeToGL(mode);
- }
-}
-
-static GLenum gmsBlendModeToGLDFactor(int mode) {
- switch (mode) {
- case bm_normal:
- return GL_ONE_MINUS_SRC_ALPHA;
- case bm_add:
- return GL_ONE;
- case bm_subtract:
- return GL_ONE_MINUS_SRC_COLOR;
- case bm_reverse_subtract:
- return GL_ONE;
- case bm_min:
- return GL_ONE;
- case bm_max:
- return GL_ONE_MINUS_SRC_COLOR;
- default:
- return gmsBlendModeToGL(mode);
- }
-}
-
-static void glGpuSetBlendMode(Renderer* renderer, int32_t mode) {
- flushBatch((GLRenderer*)renderer);
- glBlendEquation(
- gmsBlendModeToGLEquation(mode)
- );
- glBlendFunc(
- gmsBlendModeToGLSFactor(mode),
- gmsBlendModeToGLDFactor(mode)
- );
-}
-
-static void glGpuSetBlendModeExt(Renderer* renderer, int32_t sfactor, int32_t dfactor) {
- flushBatch((GLRenderer*)renderer);
- glBlendFunc(
- gmsBlendModeToGLSFactor(sfactor),
- gmsBlendModeToGLDFactor(dfactor)
- );
-}
-
-static void glGpuSetBlendEnable(Renderer* renderer, bool enable) {
- flushBatch((GLRenderer*)renderer);
- enable ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
-}
-
-static void glGpuSetAlphaTestEnable(Renderer* renderer, bool enable) {
- GLRenderer* gl = (GLRenderer*) renderer;
- if (gl->alphaTestEnable == enable) return;
- flushBatch(gl);
- gl->alphaTestEnable = enable;
- glUseProgram(gl->shaderProgram);
- glUniform1f(gl->uAlphaTestRef, enable ? gl->alphaTestRef : -1.0f);
-}
-
-static void glGpuSetAlphaTestRef(Renderer* renderer, uint8_t ref) {
- GLRenderer* gl = (GLRenderer*) renderer;
- float refF = ref / 255.0f;
- if (gl->alphaTestRef == refF) return;
- flushBatch(gl);
- gl->alphaTestRef = refF;
- if (gl->alphaTestEnable) {
- glUseProgram(gl->shaderProgram);
- glUniform1f(gl->uAlphaTestRef, refF);
- }
-}
-
-static void glGpuSetColorWriteEnable(Renderer* renderer, bool red, bool green, bool blue, bool alpha) {
- flushBatch((GLRenderer*)renderer);
- glColorMask(red, green, blue, alpha);
-}
-
-// ===[ Vtable ]===
-
-static RendererVtable glVtable = {
- .init = glInit,
- .destroy = glDestroy,
- .beginFrame = glBeginFrame,
- .endFrame = glEndFrame,
- .beginView = glBeginView,
- .endView = glEndView,
- .beginGUI = glBeginGUI,
- .endGUI = glEndGUI,
- .drawSprite = glDrawSprite,
- .drawSpritePos = glDrawSpritePos,
- .drawSpritePart = glDrawSpritePart,
- .drawRectangle = glDrawRectangle,
- .drawLine = glDrawLine,
- .drawLineColor = glDrawLineColor,
- .drawTriangle = glDrawTriangle,
- .drawText = glDrawText,
- .drawTextColor = glDrawTextColor,
- .flush = glRendererFlush,
- .createSpriteFromSurface = glCreateSpriteFromSurface,
- .deleteSprite = glDeleteSprite,
- .gpuSetBlendMode = glGpuSetBlendMode,
- .gpuSetBlendModeExt = glGpuSetBlendModeExt,
- .gpuSetBlendEnable = glGpuSetBlendEnable,
- .gpuSetAlphaTestEnable = glGpuSetAlphaTestEnable,
- .gpuSetAlphaTestRef = glGpuSetAlphaTestRef,
- .gpuSetColorWriteEnable = glGpuSetColorWriteEnable,
- .drawTile = nullptr,
-};
-
-// ===[ Public API ]===
-
-Renderer* GLRenderer_create(void) {
- GLRenderer* gl = safeCalloc(1, sizeof(GLRenderer));
- gl->base.vtable = &glVtable;
- gl->base.drawColor = 0xFFFFFF; // white (BGR)
- gl->base.drawAlpha = 1.0f;
- gl->base.drawFont = -1;
- gl->base.drawHalign = 0;
- gl->base.drawValign = 0;
- return (Renderer*) gl;
-}
diff --git a/src/gl/gl_renderer.h b/src/gl/gl_renderer.h
deleted file mode 100644
index 02b944cc..00000000
--- a/src/gl/gl_renderer.h
+++ /dev/null
@@ -1,50 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "renderer.h"
-#include
-
-// ===[ GLRenderer Struct ]===
-// Exposed in the header so platform-specific code (main.c) can access FBO fields for screenshots.
-typedef struct {
- Renderer base; // Must be first field for struct embedding
-
- GLuint shaderProgram;
- GLint uProjection;
- GLint uTexture;
- GLint uAlphaTestRef;
-
- bool alphaTestEnable;
- float alphaTestRef;
-
- GLuint vao, vbo, ebo;
- float* vertexData; // MAX_QUADS * VERTICES_PER_QUAD * FLOATS_PER_VERTEX floats
-
- int32_t quadCount;
- GLuint currentTextureId;
-
- GLuint* glTextures; // one GL texture per TXTR page
- int32_t* textureWidths; // needed for UV normalization
- int32_t* textureHeights;
- bool* textureLoaded; // lazy loading: true once PNG decoded and uploaded
- uint32_t textureCount;
-
- GLuint whiteTexture; // 1x1 white pixel for drawing primitives (rectangles, lines, etc.)
-
- // FBO for render-to-texture (game renders here, then blitted to screen)
- GLuint fbo;
- GLuint fboTexture;
- int32_t fboWidth;
- int32_t fboHeight;
- int32_t windowW; // stored from beginFrame for endFrame blit
- int32_t windowH;
- int32_t gameW; // game resolution (for FBO sizing)
- int32_t gameH;
-
- // Original counts from data.win (dynamic slots start at these indices)
- uint32_t originalTexturePageCount;
- uint32_t originalTpagCount;
- uint32_t originalSpriteCount;
-} GLRenderer;
-
-Renderer* GLRenderer_create(void);
diff --git a/src/gl/image_decoder.c b/src/gl/image_decoder.c
index 16b2aacc..756b47df 100644
--- a/src/gl/image_decoder.c
+++ b/src/gl/image_decoder.c
@@ -3,7 +3,14 @@
#include
#include
#include
+
+#ifndef IMAGE_DECODER_HAS_BZ2
+#define IMAGE_DECODER_HAS_BZ2 1
+#endif
+
+#if IMAGE_DECODER_HAS_BZ2
#include
+#endif
#include "stb_image.h"
@@ -119,6 +126,15 @@ static uint8_t* decodeQoi(const uint8_t* data, size_t dataSize, int* outW, int*
// bytes 8..11 = uncompressed BZ2 length (LE uint32) -- ONLY when gm2022_5 is true
// bytes 8.. (or 12.. if gm2022_5) = raw BZip2 stream, which decompresses into a full "fioq" QOI file.
static uint8_t* decodeBz2Qoi(const uint8_t* blob, size_t blobSize, bool gm2022_5, int* outW, int* outH) {
+#if !IMAGE_DECODER_HAS_BZ2
+ (void) blob;
+ (void) blobSize;
+ (void) gm2022_5;
+ (void) outW;
+ (void) outH;
+ fprintf(stderr, "ImageDecoder: BZip2 support is disabled in this build\n");
+ return nullptr;
+#else
size_t headerSize = gm2022_5 ? COMPRESSED_QOI_HEADER_SIZE_NEW : COMPRESSED_QOI_HEADER_SIZE_OLD;
if (headerSize > blobSize) return nullptr;
@@ -142,6 +158,7 @@ static uint8_t* decodeBz2Qoi(const uint8_t* blob, size_t blobSize, bool gm2022_5
uint8_t* result = decodeQoi(uncompressed, destLen, outW, outH);
free(uncompressed);
return result;
+#endif
}
uint8_t* ImageDecoder_decodeToRgba(const uint8_t* blob, size_t blobSize, bool gm2022_5, int* outW, int* outH) {
@@ -164,4 +181,4 @@ uint8_t* ImageDecoder_decodeToRgba(const uint8_t* blob, size_t blobSize, bool gm
*outW = w;
*outH = h;
return pixels;
-}
+}
\ No newline at end of file
diff --git a/src/glfw/gl_legacy_renderer.c b/src/glfw/gl_legacy_renderer.c
deleted file mode 100644
index c3bb8a22..00000000
--- a/src/glfw/gl_legacy_renderer.c
+++ /dev/null
@@ -1,1260 +0,0 @@
-#include "gl_legacy_renderer.h"
-#include "matrix_math.h"
-#include "text_utils.h"
-
-#include
-#include
-#include
-#include
-#include
-
-#include "stb_image.h"
-#include "stb_ds.h"
-#include "utils.h"
-#include "image_decoder.h"
-
-// ===[ Helpers ]===
-static void glApplyViewport(GLLegacyRenderer* gl, int32_t x, int32_t y, int32_t w, int32_t h) {
- int32_t effW, effH;
- if ((gl->gameW * gl->windowH) / gl->gameH < gl->windowW) {
- effW = (gl->gameW * gl->windowH) / gl->gameH;
- effH = gl->windowH;
- } else {
- effW = gl->windowW;
- effH = (gl->gameH * gl->windowW) / gl->gameW;
- }
- float scale = (float)effW / (float)gl->gameW;
- int32_t offsetX = (gl->windowW - effW) / 2;
- int32_t offsetY = (gl->windowH - effH) / 2;
-
- int32_t vpX = offsetX + (int32_t)(x * scale);
- int32_t vpY = offsetY + (int32_t)((gl->gameH - y - h) * scale);
- int32_t vpW = (int32_t)(w * scale);
- int32_t vpH = (int32_t)(h * scale);
-
- glViewport(vpX, vpY, vpW, vpH);
- glEnable(GL_SCISSOR_TEST);
- glScissor(vpX, vpY, vpW, vpH);
-}
-
-// ===[ Vtable Implementations ]===
-
-static void glInit(Renderer* renderer, DataWin* dataWin) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- renderer->dataWin = dataWin;
-
- // Prepare texture slots for lazy loading (PNG decode deferred to first use)
- glEnable(GL_TEXTURE_2D);
- glDisable(GL_DEPTH_TEST);
- glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-
- gl->textureCount = dataWin->txtr.count;
- gl->glTextures = safeMalloc(gl->textureCount * sizeof(GLuint));
- gl->textureWidths = safeMalloc(gl->textureCount * sizeof(int32_t));
- gl->textureHeights = safeMalloc(gl->textureCount * sizeof(int32_t));
- gl->textureLoaded = safeMalloc(gl->textureCount * sizeof(bool));
-
- glGenTextures((GLsizei) gl->textureCount, gl->glTextures);
-
- for (uint32_t i = 0; gl->textureCount > i; i++) {
- gl->textureWidths[i] = 0;
- gl->textureHeights[i] = 0;
- gl->textureLoaded[i] = false;
- }
-
- // Create 1x1 white pixel texture for primitive drawing (rectangles, lines, etc.)
- glGenTextures(1, &gl->whiteTexture);
- glBindTexture(GL_TEXTURE_2D, gl->whiteTexture);
- uint8_t whitePixel[4] = {255, 255, 255, 255};
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-
- // Enable blending
- glEnable(GL_BLEND);
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-
- glBindTexture(GL_TEXTURE_2D, 0);
-
- // Save original counts so we know which slots are from data.win vs dynamic
- gl->originalTexturePageCount = gl->textureCount;
- gl->originalTpagCount = dataWin->tpag.count;
- gl->originalSpriteCount = dataWin->sprt.count;
-
- fprintf(stderr, "GL: Renderer initialized (%u texture pages)\n", gl->textureCount);
-}
-
-static void glDestroy(Renderer* renderer) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- glDeleteTextures(1, &gl->whiteTexture);
-
- glDeleteTextures((GLsizei) gl->textureCount, gl->glTextures);
-
- free(gl->glTextures);
- free(gl->textureWidths);
- free(gl->textureHeights);
- free(gl);
-}
-
-static void glBeginFrame(Renderer* renderer, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- gl->windowW = windowW;
- gl->windowH = windowH;
- gl->gameW = gameW;
- gl->gameH = gameH;
-
- glApplyViewport(gl, 0, 0, gameW, gameH);
- glBindTexture(GL_TEXTURE_2D, 0);
-}
-
-static void glBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- glBindTexture(GL_TEXTURE_2D, 0);
-
- // Set viewport and scissor to the port rectangle within the FBO
- // FBO uses game resolution, port coordinates are in game space
- // OpenGL viewport Y is bottom-up, game Y is top-down
- glApplyViewport(gl, portX, portY, portW, portH);
-
- // Build orthographic projection (Y-down for GML coordinate system)
- Matrix4f projection;
- Matrix4f_identity(&projection);
- Matrix4f_ortho(&projection, (float) viewX, (float) (viewX + viewW), (float) (viewY + viewH), (float) viewY, -1.0f, 1.0f);
-
- if (viewAngle != 0.0f) {
- // GML view_angle: rotate camera by this angle (degrees, counter-clockwise)
- // To rotate the camera, we rotate the world in the opposite direction around the view center
- float cx = (float) viewX + (float) viewW / 2.0f;
- float cy = (float) viewY + (float) viewH / 2.0f;
- Matrix4f rot;
- Matrix4f_identity(&rot);
- Matrix4f_translate(&rot, cx, cy, 0.0f);
- float angleRad = viewAngle * (float) M_PI / 180.0f;
- Matrix4f_rotateZ(&rot, -angleRad);
- Matrix4f_translate(&rot, -cx, -cy, 0.0f);
- Matrix4f result;
- Matrix4f_multiply(&result, &projection, &rot);
- projection = result;
- }
-
- glMatrixMode(GL_PROJECTION);
- glLoadMatrixf(projection.m);
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
- glActiveTexture(GL_TEXTURE0);
-}
-
-static void glEndView(MAYBE_UNUSED Renderer* renderer) {
- glDisable(GL_SCISSOR_TEST);
-}
-
-static void glBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- glBindTexture(GL_TEXTURE_2D, 0);
-
- glApplyViewport(gl, portX, portY, portW, portH);
-
- Matrix4f projection;
- Matrix4f_identity(&projection);
- Matrix4f_ortho(&projection, 0.0f, (float) guiW, (float) guiH, 0.0f, -1.0f, 1.0f);
-
- glMatrixMode(GL_PROJECTION);
- glLoadMatrixf(projection.m);
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
- glActiveTexture(GL_TEXTURE0);
-}
-
-static void glEndGUI(MAYBE_UNUSED Renderer* renderer) {
- glDisable(GL_SCISSOR_TEST);
-}
-
-static void glEndFrame(Renderer* renderer) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- int effectiveEndX, effectiveEndY;
- int effectiveStartX, effectiveStartY;
-
- // Try and match the "intended" aspect ratio as closely
- // as possible while still fitting on the screen
- if ((gl->gameW * gl->windowH) / gl->gameH < gl->windowW) {
- effectiveEndX = (gl->gameW * gl->windowH) / gl->gameH;
- effectiveEndY = gl->windowH;
- } else {
- effectiveEndX = gl->windowW;
- effectiveEndY = (gl->gameH * gl->windowW) / gl->gameW;
- }
- effectiveStartX = (gl->windowW - effectiveEndX) / 2;
- effectiveStartY = (gl->windowH - effectiveEndY) / 2;
- effectiveEndX += effectiveStartX;
- effectiveEndY += effectiveStartY;
-}
-
-static void glRendererFlush(MAYBE_UNUSED Renderer* renderer) {}
-
-// Lazily decodes and uploads a TXTR page on first access.
-// Returns true if the texture is ready, false if it failed to decode.
-static bool ensureTextureLoaded(GLLegacyRenderer* gl, uint32_t pageId) {
- if (gl->textureLoaded[pageId]) return (gl->textureWidths[pageId] != 0);
-
- gl->textureLoaded[pageId] = true;
-
- DataWin* dw = gl->base.dataWin;
- Texture* txtr = &dw->txtr.textures[pageId];
-
- int w, h;
- bool gm2022_5 = DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0);
- uint8_t* pixels = ImageDecoder_decodeToRgba(txtr->blobData, (size_t) txtr->blobSize, gm2022_5, &w, &h);
- if (pixels == nullptr) {
- fprintf(stderr, "GL: Failed to decode TXTR page %u\n", pageId);
- return false;
- }
-
- gl->textureWidths[pageId] = w;
- gl->textureHeights[pageId] = h;
-
- glBindTexture(GL_TEXTURE_2D, gl->glTextures[pageId]);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-
- free(pixels);
- fprintf(stderr, "GL: Loaded TXTR page %u (%dx%d)\n", pageId, w, h);
- return true;
-}
-
-static void glDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return;
-
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- int16_t pageId = tpag->texturePageId;
- if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return;
- if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return;
-
- GLuint texId = gl->glTextures[pageId];
- int32_t texW = gl->textureWidths[pageId];
- int32_t texH = gl->textureHeights[pageId];
-
- glBindTexture(GL_TEXTURE_2D, texId);
-
- // Compute normalized UVs from TPAG source rect
- float u0 = (float) tpag->sourceX / (float) texW;
- float v0 = (float) tpag->sourceY / (float) texH;
- float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW;
- float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH;
-
- // Compute local quad corners (relative to origin, with target offset)
- float localX0 = (float) tpag->targetX - originX;
- float localY0 = (float) tpag->targetY - originY;
- float localX1 = localX0 + (float) tpag->sourceWidth;
- float localY1 = localY0 + (float) tpag->sourceHeight;
-
- // Build 2D transform: T(x,y) * R(-angleDeg) * S(xscale, yscale)
- // GML rotation is counter-clockwise, OpenGL rotation is counter-clockwise, but
- // since we have Y-down, we negate the angle to get the correct visual rotation
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- Matrix4f transform;
- Matrix4f_setTransform2D(&transform, x, y, xscale, yscale, angleRad);
-
- // Transform 4 corners
- float x0, y0, x1, y1, x2, y2, x3, y3;
- Matrix4f_transformPoint(&transform, localX0, localY0, &x0, &y0); // top-left
- Matrix4f_transformPoint(&transform, localX1, localY0, &x1, &y1); // top-right
- Matrix4f_transformPoint(&transform, localX1, localY1, &x2, &y2); // bottom-right
- Matrix4f_transformPoint(&transform, localX0, localY1, &x3, &y3); // bottom-left
-
- // Convert BGR color to RGB floats
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- glBegin(GL_QUADS);
- // Vertex 0: top-left
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u0, v0);
- glVertex2f(x0, y0);
-
- // Vertex 1: top-right
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u1, v0);
- glVertex2f(x1, y1);
-
- // Vertex 2: bottom-right
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u1, v1);
- glVertex2f(x2, y2);
-
- // Vertex 3: bottom-left
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u0, v1);
- glVertex2f(x3, y3);
- glEnd();
-}
-
-static void glDrawSpritePos(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return;
-
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- int16_t pageId = tpag->texturePageId;
- if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return;
- if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return;
-
- GLuint texId = gl->glTextures[pageId];
- int32_t texW = gl->textureWidths[pageId];
- int32_t texH = gl->textureHeights[pageId];
- glBindTexture(GL_TEXTURE_2D, texId);
-
- float u0 = (float) tpag->sourceX / (float) texW;
- float v0 = (float) tpag->sourceY / (float) texH;
- float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW;
- float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH;
-
- glBegin(GL_QUADS);
- glColor4f(1.0f, 1.0f, 1.0f, alpha);
- glTexCoord2f(u0, v0);
- glVertex2f(x1, y1);
-
- glColor4f(1.0f, 1.0f, 1.0f, alpha);
- glTexCoord2f(u1, v0);
- glVertex2f(x2, y2);
-
- glColor4f(1.0f, 1.0f, 1.0f, alpha);
- glTexCoord2f(u1, v1);
- glVertex2f(x3, y3);
-
- glColor4f(1.0f, 1.0f, 1.0f, alpha);
- glTexCoord2f(u0, v1);
- glVertex2f(x4, y4);
- glEnd();
-}
-
-static void glDrawSpritePart(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return;
-
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- int16_t pageId = tpag->texturePageId;
- if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return;
- if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return;
-
- GLuint texId = gl->glTextures[pageId];
- int32_t texW = gl->textureWidths[pageId];
- int32_t texH = gl->textureHeights[pageId];
-
- glBindTexture(GL_TEXTURE_2D, texId);
-
- // Compute UVs for the sub-region within the atlas
- float u0 = (float) (tpag->sourceX + srcOffX) / (float) texW;
- float v0 = (float) (tpag->sourceY + srcOffY) / (float) texH;
- float u1 = (float) (tpag->sourceX + srcOffX + srcW) / (float) texW;
- float v1 = (float) (tpag->sourceY + srcOffY + srcH) / (float) texH;
-
- // Convert BGR color to RGB floats
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- // Quad corners (no origin offset - draw_sprite_part ignores sprite origin)
- float cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3;
- if (angleDeg == 0.0f) {
- cx0 = x; cy0 = y;
- cx1 = x + (float) srcW * xscale; cy1 = y;
- cx2 = x + (float) srcW * xscale; cy2 = y + (float) srcH * yscale;
- cx3 = x; cy3 = y + (float) srcH * yscale;
- } else {
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- float cosA = cosf(angleRad);
- float sinA = sinf(angleRad);
- float qx0 = x, qy0 = y;
- float qx1 = x + (float) srcW * xscale, qy1 = y;
- float qx2 = x + (float) srcW * xscale, qy2 = y + (float) srcH * yscale;
- float qx3 = x, qy3 = y + (float) srcH * yscale;
- float dx, dy;
- dx = qx0 - pivotX; dy = qy0 - pivotY; cx0 = cosA * dx - sinA * dy + pivotX; cy0 = sinA * dx + cosA * dy + pivotY;
- dx = qx1 - pivotX; dy = qy1 - pivotY; cx1 = cosA * dx - sinA * dy + pivotX; cy1 = sinA * dx + cosA * dy + pivotY;
- dx = qx2 - pivotX; dy = qy2 - pivotY; cx2 = cosA * dx - sinA * dy + pivotX; cy2 = sinA * dx + cosA * dy + pivotY;
- dx = qx3 - pivotX; dy = qy3 - pivotY; cx3 = cosA * dx - sinA * dy + pivotX; cy3 = sinA * dx + cosA * dy + pivotY;
- }
-
- glBegin(GL_QUADS);
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u0, v0); glVertex2f(cx0, cy0);
-
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u1, v0); glVertex2f(cx1, cy1);
-
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u1, v1); glVertex2f(cx2, cy2);
-
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u0, v1); glVertex2f(cx3, cy3);
- glEnd();
-}
-
-// Emits a single colored quad into the batch using the white pixel texture
-static void emitColoredQuad(GLLegacyRenderer* gl, float x0, float y0, float x1, float y1, float r, float g, float b, float a) {
- glBindTexture(GL_TEXTURE_2D, gl->whiteTexture);
-
- glBegin(GL_QUADS);
- // All UVs point to (0.5, 0.5) center of the 1x1 white texture
- // Vertex 0: top-left
- glColor4f(r, g, b, a);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x0, y0);
-
- // Vertex 1: top-right
- glColor4f(r, g, b, a);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x1, y0);
-
- // Vertex 2: bottom-right
- glColor4f(r, g, b, a);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x1, y1);
-
- // Vertex 3: bottom-left
- glColor4f(r, g, b, a);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x0, y1);
- glEnd();
-}
-
-static void glDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- if (outline) {
- // Draw 4 one-pixel-wide edges: top, bottom, left, right
- emitColoredQuad(gl, x1, y1, x2 + 1, y1 + 1, r, g, b, alpha); // top
- emitColoredQuad(gl, x1, y2, x2 + 1, y2 + 1, r, g, b, alpha); // bottom
- emitColoredQuad(gl, x1, y1 + 1, x1 + 1, y2, r, g, b, alpha); // left
- emitColoredQuad(gl, x2, y1 + 1, x2 + 1, y2, r, g, b, alpha); // right
- } else {
- // Filled rectangle: GML adds +1 to width/height for filled rects
- emitColoredQuad(gl, x1, y1, x2 + 1, y2 + 1, r, g, b, alpha);
- }
-}
-
-// ===[ Line Drawing ]===
-
-static void glDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- // Compute perpendicular offset for line thickness
- float dx = x2 - x1;
- float dy = y2 - y1;
- float len = sqrtf(dx * dx + dy * dy);
- if (0.0001f > len) return;
-
- float halfW = width * 0.5f;
- float px = (-dy / len) * halfW;
- float py = (dx / len) * halfW;
-
- glBindTexture(GL_TEXTURE_2D, gl->whiteTexture);
-
- // Vertex 0: start + perpendicular
- glBegin(GL_QUADS);
- glColor4f(r, g, b, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x1 + px, y1 + py);
-
- // Vertex 1: start - perpendicular
- glColor4f(r, g, b, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x1 - px, y1 - py);
-
- // Vertex 2: end - perpendicular
- glColor4f(r, g, b, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x2 - px, y2 - py);
-
- // Vertex 3: end + perpendicular
- glColor4f(r, g, b, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x2 + px, y2 + py);
- glEnd();
-}
-
-static void glDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
-
- float r1 = (float) BGR_R(color1) / 255.0f;
- float g1 = (float) BGR_G(color1) / 255.0f;
- float b1 = (float) BGR_B(color1) / 255.0f;
-
- float r2 = (float) BGR_R(color2) / 255.0f;
- float g2 = (float) BGR_G(color2) / 255.0f;
- float b2 = (float) BGR_B(color2) / 255.0f;
-
- // Compute perpendicular offset for line thickness
- float dx = x2 - x1;
- float dy = y2 - y1;
- float len = sqrtf(dx * dx + dy * dy);
- if (0.0001f > len) return;
-
- float halfW = width * 0.5f;
- float px = (-dy / len) * halfW;
- float py = (dx / len) * halfW;
-
- // Emit quad with per-vertex colors (color1 at start, color2 at end)
- glBindTexture(GL_TEXTURE_2D, gl->whiteTexture);
-
- glBegin(GL_QUADS);
- // Vertex 0: start + perpendicular (color1)
- glColor4f(r1, g1, b1, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x1 + px, y1 + py);
-
- // Vertex 1: start - perpendicular (color1)
- glColor4f(r1, g1, b1, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x1 - px, y1 - py);
-
- // Vertex 2: end - perpendicular (color2)
- glColor4f(r2, g2, b2, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x2 - px, y2 - py);
-
- // Vertex 3: end + perpendicular (color2)
- glColor4f(r2, g2, b2, alpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x2 + px, y2 + py);
- glEnd();
-}
-
-static void glDrawTriangle(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline)
-{
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- if(outline)
- {
- glDrawLine(renderer, x1, y1, x2, y2, 1, renderer->drawColor, 1.0);
- glDrawLine(renderer, x2, y2, x3, y3, 1, renderer->drawColor, 1.0);
- glDrawLine(renderer, x3, y3, x1, y1, 1, renderer->drawColor, 1.0);
- } else {
- float r = (float) BGR_R(renderer->drawColor) / 255.0f;
- float g = (float) BGR_G(renderer->drawColor) / 255.0f;
- float b = (float) BGR_B(renderer->drawColor) / 255.0f;
-
- glBindTexture(GL_TEXTURE_2D, gl->whiteTexture);
-
- glBegin(GL_TRIANGLES);
- glColor4f(r, g, b, renderer->drawAlpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x1 , y1);
-
- glColor4f(r, g, b, renderer->drawAlpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x2, y2);
-
- glColor4f(r, g, b, renderer->drawAlpha);
- glTexCoord2f(0.5f, 0.5f);
- glVertex2f(x3, y3);
- glEnd();
- }
-}
-
-// ===[ Text Drawing ]===
-
-// Resolved font state shared between glDrawText and glDrawTextColor
-typedef struct {
- Font* font;
- TexturePageItem* fontTpag; // single TPAG for regular fonts (nullptr for sprite fonts)
- GLuint texId;
- int32_t texW, texH;
- Sprite* spriteFontSprite; // source sprite for sprite fonts (nullptr for regular fonts)
-} GlFontState;
-
-// Resolves font texture state
-// Returns false if the font can't be drawn
-static bool glResolveFontState(GLLegacyRenderer* gl, DataWin* dw, Font* font, GlFontState* state) {
- state->font = font;
- state->fontTpag = nullptr;
- state->texId = 0;
- state->texW = 0;
- state->texH = 0;
- state->spriteFontSprite = nullptr;
-
- if (!font->isSpriteFont) {
- int32_t fontTpagIndex = font->tpagIndex;
- if (0 > fontTpagIndex) return false;
-
- state->fontTpag = &dw->tpag.items[fontTpagIndex];
- int16_t pageId = state->fontTpag->texturePageId;
- if (0 > pageId || (uint32_t) pageId >= gl->textureCount) return false;
- if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return false;
-
- state->texId = gl->glTextures[pageId];
- state->texW = gl->textureWidths[pageId];
- state->texH = gl->textureHeights[pageId];
- } else if (font->spriteIndex >= 0 && dw->sprt.count > (uint32_t) font->spriteIndex) {
- state->spriteFontSprite = &dw->sprt.sprites[font->spriteIndex];
- }
- return true;
-}
-
-// Resolves UV coordinates, texture ID, and local position for a single glyph
-// Returns false if the glyph can't be drawn
-static bool glResolveGlyph(GLLegacyRenderer* gl, DataWin* dw, GlFontState* state, FontGlyph* glyph, float cursorX, float cursorY, GLuint* outTexId, float* outU0, float* outV0, float* outU1, float* outV1, float* outLocalX0, float* outLocalY0) {
- Font* font = state->font;
- if (font->isSpriteFont && state->spriteFontSprite != nullptr) {
- Sprite* sprite = state->spriteFontSprite;
- int32_t glyphIndex = (int32_t) (glyph - font->glyphs);
- if (0 > glyphIndex || glyphIndex >= (int32_t) sprite->textureCount) return false;
-
- int32_t tpagIdx = sprite->tpagIndices[glyphIndex];
- if (0 > tpagIdx) return false;
-
- TexturePageItem* glyphTpag = &dw->tpag.items[tpagIdx];
- int16_t pid = glyphTpag->texturePageId;
- if (0 > pid || (uint32_t) pid >= gl->textureCount) return false;
- if (!ensureTextureLoaded(gl, (uint32_t) pid)) return false;
-
- *outTexId = gl->glTextures[pid];
- int32_t tw = gl->textureWidths[pid];
- int32_t th = gl->textureHeights[pid];
-
- *outU0 = (float) glyphTpag->sourceX / (float) tw;
- *outV0 = (float) glyphTpag->sourceY / (float) th;
- *outU1 = (float) (glyphTpag->sourceX + glyphTpag->sourceWidth) / (float) tw;
- *outV1 = (float) (glyphTpag->sourceY + glyphTpag->sourceHeight) / (float) th;
-
- *outLocalX0 = cursorX + (float) glyph->offset;
- *outLocalY0 = cursorY + (float) ((int32_t) glyphTpag->targetY - sprite->originY);
- } else {
- *outTexId = state->texId;
- *outU0 = (float) (state->fontTpag->sourceX + glyph->sourceX) / (float) state->texW;
- *outV0 = (float) (state->fontTpag->sourceY + glyph->sourceY) / (float) state->texH;
- *outU1 = (float) (state->fontTpag->sourceX + glyph->sourceX + glyph->sourceWidth) / (float) state->texW;
- *outV1 = (float) (state->fontTpag->sourceY + glyph->sourceY + glyph->sourceHeight) / (float) state->texH;
-
- *outLocalX0 = cursorX + glyph->offset;
- *outLocalY0 = cursorY;
- }
- return true;
-}
-
-static void glDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- int32_t fontIndex = renderer->drawFont;
- if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return;
-
- Font* font = &dw->font.fonts[fontIndex];
-
- GlFontState fontState;
- if (!glResolveFontState(gl, dw, font, &fontState)) return;
-
- uint32_t color = renderer->drawColor;
- float alpha = renderer->drawAlpha;
- float r = (float) BGR_R(color) / 255.0f;
- float g = (float) BGR_G(color) / 255.0f;
- float b = (float) BGR_B(color) / 255.0f;
-
- int32_t textLen = (int32_t) strlen(text);
-
- // Count lines, treating \r\n and \n\r as single breaks
- int32_t lineCount = TextUtils_countLines(text, textLen);
-
- // Per-line vertical stride. HTML5 runner's default `linesep` is `max_glyph_height * scaleY`.
- // We apply scaleY via the transform matrix below, so keep the stride in pre-scale (local) coords.
- float lineStride = TextUtils_lineStride(font);
-
- // Vertical alignment offset
- float totalHeight = (float) lineCount * lineStride;
- float valignOffset = 0;
- if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f;
- else if (renderer->drawValign == 2) valignOffset = -totalHeight;
-
- // Build transform matrix
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- Matrix4f transform;
- Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad);
-
- // Iterate through lines. HTML5 subtracts ascenderOffset from the per-line y offset
- // (see yyFont.GR_Text_Draw), shifting glyphs up so the baseline aligns with the drawn y.
- float cursorY = valignOffset - (float) font->ascenderOffset;
- int32_t lineStart = 0;
-
- for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) {
- // Find end of current line
- int32_t lineEnd = lineStart;
- while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) {
- lineEnd++;
- }
- int32_t lineLen = lineEnd - lineStart;
-
- // Horizontal alignment offset for this line
- float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen);
- float halignOffset = 0;
- if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f;
- else if (renderer->drawHalign == 2) halignOffset = -lineWidth;
-
- float cursorX = halignOffset;
-
- // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning)
- int32_t pos = 0;
- uint16_t ch = 0;
- bool hasCh = false;
- if (lineLen > pos) {
- ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
- hasCh = true;
- }
-
- while (hasCh) {
- FontGlyph* glyph = TextUtils_findGlyph(font, ch);
-
- uint16_t nextCh = 0;
- bool hasNext = lineLen > pos;
- if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
-
- if (glyph != nullptr) {
- bool drewSuccessfully = false;
- if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) {
- float u0, v0, u1, v1;
- float localX0, localY0;
- GLuint glyphTexId;
-
- if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) {
- glBindTexture(GL_TEXTURE_2D, glyphTexId);
-
- float localX1 = localX0 + (float) glyph->sourceWidth;
- float localY1 = localY0 + (float) glyph->sourceHeight;
-
- // Transform corners
- float px0, py0, px1, py1, px2, py2, px3, py3;
- Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0);
- Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1);
- Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2);
- Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3);
-
- glBegin(GL_QUADS);
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u0, v0);
- glVertex2f(px0, py0);
-
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u1, v0);
- glVertex2f(px1, py1);
-
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u1, v1);
- glVertex2f(px2, py2);
-
- glColor4f(r, g, b, alpha);
- glTexCoord2f(u0, v1);
- glVertex2f(px3, py3);
- glEnd();
-
- drewSuccessfully = true;
- }
- }
-
- cursorX += glyph->shift;
- if (drewSuccessfully && hasNext) {
- cursorX += TextUtils_getKerningOffset(glyph, nextCh);
- }
- }
-
- ch = nextCh;
- hasCh = hasNext;
- }
-
- cursorY += lineStride;
- // Skip past the newline, treating \r\n and \n\r as single breaks
- if (textLen > lineEnd) {
- lineStart = TextUtils_skipNewline(text, lineEnd, textLen);
- } else {
- lineStart = lineEnd;
- }
- }
-}
-
-static void glDrawTextColor(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t _c1, int32_t _c2, int32_t _c3, int32_t _c4, float alpha) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- int32_t fontIndex = renderer->drawFont;
- if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return;
-
- Font* font = &dw->font.fonts[fontIndex];
-
- GlFontState fontState;
- if (!glResolveFontState(gl, dw, font, &fontState)) return;
-
- int32_t textLen = (int32_t) strlen(text);
- if(textLen == 0) return;
-
- // Count lines, treating \r\n and \n\r as single breaks
- int32_t lineCount = TextUtils_countLines(text, textLen);
-
- float lineStride = TextUtils_lineStride(font);
-
- // Vertical alignment offset
- float totalHeight = (float) lineCount * lineStride;
- float valignOffset = 0;
- if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f;
- else if (renderer->drawValign == 2) valignOffset = -totalHeight;
-
- // Build transform matrix
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- Matrix4f transform;
- Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad);
-
- // Iterate through lines. HTML5 subtracts ascenderOffset from per-line y offset.
- float cursorY = valignOffset - (float) font->ascenderOffset;
- int32_t lineStart = 0;
-
- // get delta's (16.16 format)
- int32_t left_r_dx = ((_c2 & 0xff0000) - (_c1 & 0xff0000)) / textLen;
- int32_t left_g_dx = ((((_c2 & 0xff00) << 8) - ((_c1 & 0xff00) << 8))) / textLen;
- int32_t left_b_dx = ((((_c2 & 0xff) << 16) - ((_c1 & 0xff) << 16))) / textLen;
-
- int32_t right_r_dx = ((_c3 & 0xff0000) - (_c4 & 0xff0000)) / textLen;
- int32_t right_g_dx = ((((_c3 & 0xff00) << 8) - ((_c4 & 0xff00) << 8))) / textLen;
- int32_t right_b_dx = ((((_c3 & 0xff) << 16) - ((_c4 & 0xff) << 16))) / textLen;
-
- int32_t left_delta_r = left_r_dx;
- int32_t left_delta_g = left_g_dx;
- int32_t left_delta_b = left_b_dx;
- int32_t right_delta_r = right_r_dx;
- int32_t right_delta_g = right_g_dx;
- int32_t right_delta_b = right_b_dx;
-
- int32_t c1 = _c1;
- int32_t c4 = _c4;
-
- for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) {
- // Find end of current line
- int32_t lineEnd = lineStart;
- while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) {
- lineEnd++;
- }
- int32_t lineLen = lineEnd - lineStart;
-
- // Horizontal alignment offset for this line
- float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen);
- float halignOffset = 0;
- if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f;
- else if (renderer->drawHalign == 2) halignOffset = -lineWidth;
-
- float cursorX = halignOffset;
-
- // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning)
- int32_t pos = 0;
- uint16_t ch = 0;
- bool hasCh = false;
- if (lineLen > pos) {
- ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
- hasCh = true;
- }
-
- while (hasCh) {
- // do 16.16 maths
- int32_t c2 = ((c1 & 0xff0000) + (left_delta_r & 0xff0000)) & 0xff0000;
- c2 |= ((c1 & 0xff00) + (left_delta_g >> 8) & 0xff00) & 0xff00;
- c2 |= ((c1 & 0xff) + (left_delta_b >> 16)) & 0xff;
- int32_t c3 = ((c4 & 0xff0000) + (right_delta_r & 0xff0000)) & 0xff0000;
- c3 |= ((c4 & 0xff00) + (right_delta_g >> 8) & 0xff00) & 0xff00;
- c3 |= ((c4 & 0xff) + (right_delta_b >> 16)) & 0xff;
-
- left_delta_r += left_r_dx;
- left_delta_g += left_g_dx;
- left_delta_b += left_b_dx;
- right_delta_r += right_r_dx;
- right_delta_g += right_g_dx;
- right_delta_b += right_b_dx;
-
- FontGlyph* glyph = TextUtils_findGlyph(font, ch);
-
- uint16_t nextCh = 0;
- bool hasNext = lineLen > pos;
- if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos);
-
- if (glyph != nullptr) {
- bool drewSuccessfully = false;
- if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) {
- float u0, v0, u1, v1;
- float localX0, localY0;
- GLuint glyphTexId;
-
- if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) {
- glBindTexture(GL_TEXTURE_2D, glyphTexId);
-
- float localX1 = localX0 + (float) glyph->sourceWidth;
- float localY1 = localY0 + (float) glyph->sourceHeight;
-
- // Transform corners
- float px0, py0, px1, py1, px2, py2, px3, py3;
- Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0);
- Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1);
- Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2);
- Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3);
-
- glBegin(GL_QUADS);
- glColor4ub(BGR_R(c1), BGR_G(c1), BGR_B(c1), alpha * 255);
- glTexCoord2f(u0, v0);
- glVertex2f(px0, py0);
-
- glColor4ub(BGR_R(c2), BGR_G(c2), BGR_B(c2), alpha * 255);
- glTexCoord2f(u1, v0);
- glVertex2f(px1, py1);
-
- glColor4ub(BGR_R(c3), BGR_G(c3), BGR_B(c3), alpha * 255);
- glTexCoord2f(u1, v1);
- glVertex2f(px2, py2);
-
- glColor4ub(BGR_R(c4), BGR_G(c4), BGR_B(c4), alpha * 255);
- glTexCoord2f(u0, v1);
- glVertex2f(px3, py3);
- glEnd();
-
- drewSuccessfully = true;
- }
- }
-
- cursorX += glyph->shift;
- if (drewSuccessfully) {
- if (hasNext) cursorX += TextUtils_getKerningOffset(glyph, nextCh);
- c4 = c3; // set left edge to be what the last right edge was....
- c1 = c2;
- }
- }
-
- ch = nextCh;
- hasCh = hasNext;
- }
-
- cursorY += lineStride;
- // Skip past the newline, treating \r\n and \n\r as single breaks
- if (textLen > lineEnd) {
- lineStart = TextUtils_skipNewline(text, lineEnd, textLen);
- } else {
- lineStart = lineEnd;
- }
- }
-}
-
-// ===[ Dynamic Sprite Creation/Deletion ]===
-
-// Finds a free dynamic texture page slot (glTextures[i] == 0), or appends a new one.
-static uint32_t findOrAllocTexturePageSlot(GLLegacyRenderer* gl) {
- // Scan dynamic range for a reusable slot
- for (uint32_t i = gl->originalTexturePageCount; gl->textureCount > i; i++) {
- if (gl->glTextures[i] == 0) return i;
- }
- // No free slot found, grow the arrays
- uint32_t newPageId = gl->textureCount;
- gl->textureCount++;
- gl->glTextures = safeRealloc(gl->glTextures, gl->textureCount * sizeof(GLuint));
- gl->textureWidths = safeRealloc(gl->textureWidths, gl->textureCount * sizeof(int32_t));
- gl->textureHeights = safeRealloc(gl->textureHeights, gl->textureCount * sizeof(int32_t));
- gl->textureLoaded = safeRealloc(gl->textureLoaded, gl->textureCount * sizeof(bool));
- gl->glTextures[newPageId] = 0;
- gl->textureWidths[newPageId] = 0;
- gl->textureHeights[newPageId] = 0;
- gl->textureLoaded[newPageId] = false;
- return newPageId;
-}
-
-// Finds a free dynamic TPAG slot (texturePageId == -1), or appends a new one.
-static uint32_t findOrAllocTpagSlot(DataWin* dw, uint32_t originalTpagCount) {
- for (uint32_t i = originalTpagCount; dw->tpag.count > i; i++) {
- if (dw->tpag.items[i].texturePageId == -1) return i;
- }
- uint32_t newIndex = dw->tpag.count;
- dw->tpag.count++;
- dw->tpag.items = safeRealloc(dw->tpag.items, dw->tpag.count * sizeof(TexturePageItem));
- memset(&dw->tpag.items[newIndex], 0, sizeof(TexturePageItem));
- dw->tpag.items[newIndex].texturePageId = -1;
- return newIndex;
-}
-
-static int32_t glCreateSpriteFromSurface(Renderer* renderer, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 >= w || 0 >= h) return -1;
-
-
- uint8_t* pixels = safeMalloc((size_t) w * (size_t) h * 4);
- if (pixels == nullptr) return -1;
-
- // OpenGL Y is bottom-up, GML Y is top-down, so flip the Y coordinate
- int32_t glY = gl->gameH - y - h;
- glReadPixels(x, glY, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
-
- // Flip vertically (OpenGL reads bottom-to-top)
- size_t rowBytes = (size_t) w * 4;
- uint8_t* rowTemp = safeMalloc(rowBytes);
- repeat(h / 2, row) {
- uint8_t* top = pixels + row * rowBytes;
- uint8_t* bot = pixels + (h - 1 - row) * rowBytes;
- memcpy(rowTemp, top, rowBytes);
- memcpy(top, bot, rowBytes);
- memcpy(bot, rowTemp, rowBytes);
- }
- free(rowTemp);
-
- // Create a new GL texture from the captured pixels
- GLuint newTexId;
- glGenTextures(1, &newTexId);
- glBindTexture(GL_TEXTURE_2D, newTexId);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, smooth ? GL_LINEAR : GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, smooth ? GL_LINEAR : GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-
- free(pixels);
-
- // Find or allocate slots for texture page, TPAG, and sprite
- uint32_t pageId = findOrAllocTexturePageSlot(gl);
- gl->glTextures[pageId] = newTexId;
- gl->textureWidths[pageId] = w;
- gl->textureHeights[pageId] = h;
- gl->textureLoaded[pageId] = true;
-
- uint32_t tpagIndex = findOrAllocTpagSlot(dw, gl->originalTpagCount);
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- tpag->sourceX = 0;
- tpag->sourceY = 0;
- tpag->sourceWidth = (uint16_t) w;
- tpag->sourceHeight = (uint16_t) h;
- tpag->targetX = 0;
- tpag->targetY = 0;
- tpag->targetWidth = (uint16_t) w;
- tpag->targetHeight = (uint16_t) h;
- tpag->boundingWidth = (uint16_t) w;
- tpag->boundingHeight = (uint16_t) h;
- tpag->texturePageId = (int16_t) pageId;
-
- uint32_t spriteIndex = DataWin_allocSpriteSlot(dw, gl->originalSpriteCount);
- Sprite* sprite = &dw->sprt.sprites[spriteIndex];
- // name was set by DataWin_allocSpriteSlot ("__newsprite"); don't overwrite it here
- sprite->width = (uint32_t) w;
- sprite->height = (uint32_t) h;
- sprite->originX = xorig;
- sprite->originY = yorig;
- sprite->textureCount = 1;
- sprite->tpagIndices = safeMalloc(sizeof(int32_t));
- sprite->tpagIndices[0] = (int32_t) tpagIndex;
- sprite->maskCount = 0;
- sprite->masks = nullptr;
-
- fprintf(stderr, "GL: Created dynamic sprite %u (%dx%d) from surface at (%d,%d)\n", spriteIndex, w, h, x, y);
- return (int32_t) spriteIndex;
-}
-
-static void glDeleteSprite(Renderer* renderer, int32_t spriteIndex) {
- GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > spriteIndex || dw->sprt.count <= (uint32_t) spriteIndex) return;
-
- // Refuse to delete original data.win sprites
- if (gl->originalSpriteCount > (uint32_t) spriteIndex) {
- fprintf(stderr, "GL: Cannot delete data.win sprite %d\n", spriteIndex);
- return;
- }
-
- Sprite* sprite = &dw->sprt.sprites[spriteIndex];
- if (sprite->textureCount == 0) return; // already deleted
-
- // Clean up GL texture and TPAG entries owned by this sprite.
- // Slots with index >= originalTpagCount are dynamically allocated and ours to free.
- repeat(sprite->textureCount, i) {
- int32_t tpagIdx = sprite->tpagIndices[i];
- if (tpagIdx >= 0 && (uint32_t) tpagIdx >= gl->originalTpagCount) {
- TexturePageItem* tpag = &dw->tpag.items[tpagIdx];
- int16_t pageId = tpag->texturePageId;
- if (pageId >= 0 && gl->textureCount > (uint32_t) pageId) {
- glDeleteTextures(1, &gl->glTextures[pageId]);
- gl->glTextures[pageId] = 0;
- }
- // Mark TPAG slot as free for reuse
- tpag->texturePageId = -1;
- }
- }
-
- // Clear the sprite entry so it won't be drawn and can be reused. Preserve `name` across the memset: the slot is still in sprt.count and must keep a valid string for asset_get_index / name lookups.
- free(sprite->tpagIndices);
- const char* keepName = sprite->name;
- memset(sprite, 0, sizeof(Sprite));
- sprite->name = keepName;
-
- fprintf(stderr, "GL: Deleted sprite %d\n", spriteIndex);
-}
-
-static GLenum gmsBlendModeToGL(int mode) {
- switch(mode) {
- case bm_zero: return GL_ZERO;
- case bm_one: return GL_ONE;
- case bm_src_color: return GL_SRC_COLOR;
- case bm_inv_src_color: return GL_ONE_MINUS_SRC_COLOR;
- case bm_src_alpha: return GL_SRC_ALPHA;
- case bm_inv_src_alpha: return GL_ONE_MINUS_SRC_ALPHA;
- case bm_dest_alpha: return GL_DST_ALPHA;
- case bm_inv_dest_alpha: return GL_ONE_MINUS_DST_ALPHA;
- case bm_dest_color: return GL_DST_COLOR;
- case bm_inv_dest_color: return GL_ONE_MINUS_DST_COLOR;
- case bm_src_alpha_sat: return GL_SRC_ALPHA_SATURATE;
- }
- return GL_ONE;
-}
-
-static GLenum gmsBlendModeToGLEquation(int mode) {
- switch (mode) {
- case bm_normal:
- return GL_FUNC_ADD;
- case bm_add:
- return GL_FUNC_ADD;
- case bm_subtract:
- return GL_FUNC_ADD;
- case bm_reverse_subtract:
- return GL_FUNC_REVERSE_SUBTRACT;
- case bm_min:
- return GL_MIN;
- case bm_max:
- return GL_FUNC_ADD;
- default:
- return GL_FUNC_ADD;
- }
-}
-
-static GLenum gmsBlendModeToGLSFactor(int mode) {
- switch (mode) {
- case bm_normal:
- return GL_SRC_ALPHA;
- case bm_add:
- return GL_SRC_ALPHA;
- case bm_subtract:
- return GL_ZERO;
- case bm_reverse_subtract:
- return GL_SRC_ALPHA;
- case bm_min:
- return GL_ONE;
- case bm_max:
- return GL_SRC_ALPHA;
- default:
- return gmsBlendModeToGL(mode);
- }
-}
-
-static GLenum gmsBlendModeToGLDFactor(int mode) {
- switch (mode) {
- case bm_normal:
- return GL_ONE_MINUS_SRC_ALPHA;
- case bm_add:
- return GL_ONE;
- case bm_subtract:
- return GL_ONE_MINUS_SRC_COLOR;
- case bm_reverse_subtract:
- return GL_ONE;
- case bm_min:
- return GL_ONE;
- case bm_max:
- return GL_ONE_MINUS_SRC_COLOR;
- default:
- return gmsBlendModeToGL(mode);
- }
-}
-
-static void glGpuSetBlendMode(Renderer* renderer, int32_t mode) {
- glBlendEquation(
- gmsBlendModeToGLEquation(mode)
- );
- glBlendFunc(
- gmsBlendModeToGLSFactor(mode),
- gmsBlendModeToGLDFactor(mode)
- );
-}
-
-static void glGpuSetBlendModeExt(Renderer* renderer, int32_t sfactor, int32_t dfactor) {
- glBlendFunc(
- gmsBlendModeToGLSFactor(sfactor),
- gmsBlendModeToGLDFactor(dfactor)
- );
-}
-
-static void glGpuSetBlendEnable(Renderer* renderer, bool enable) {
- enable ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
-}
-
-static void glGpuSetAlphaTestEnable(Renderer* renderer, bool enable) {
- enable ? glEnable(GL_ALPHA_TEST) : glDisable(GL_ALPHA_TEST);
-}
-
-static void glGpuSetAlphaTestRef(Renderer* renderer, uint8_t ref) {
- glAlphaFunc(GL_GREATER, ref/255.0f);
-}
-
-static void glGpuSetColorWriteEnable(Renderer* renderer, bool red, bool green, bool blue, bool alpha) {
- glColorMask(red, green, blue, alpha);
-}
-
-// ===[ Vtable ]===
-
-static RendererVtable glVtable = {
- .init = glInit,
- .destroy = glDestroy,
- .beginFrame = glBeginFrame,
- .endFrame = glEndFrame,
- .beginView = glBeginView,
- .endView = glEndView,
- .beginGUI = glBeginGUI,
- .endGUI = glEndGUI,
- .drawSprite = glDrawSprite,
- .drawSpritePos = glDrawSpritePos,
- .drawSpritePart = glDrawSpritePart,
- .drawRectangle = glDrawRectangle,
- .drawLine = glDrawLine,
- .drawLineColor = glDrawLineColor,
- .drawTriangle = glDrawTriangle,
- .drawText = glDrawText,
- .drawTextColor = glDrawTextColor,
- .flush = glRendererFlush,
- .createSpriteFromSurface = glCreateSpriteFromSurface,
- .deleteSprite = glDeleteSprite,
- .gpuSetBlendMode = glGpuSetBlendMode,
- .gpuSetBlendModeExt = glGpuSetBlendModeExt,
- .gpuSetBlendEnable = glGpuSetBlendEnable,
- .gpuSetAlphaTestEnable = glGpuSetAlphaTestEnable,
- .gpuSetAlphaTestRef = glGpuSetAlphaTestRef,
- .gpuSetColorWriteEnable = glGpuSetColorWriteEnable,
- .drawTile = nullptr,
-};
-
-// ===[ Public API ]===
-
-Renderer* GLLegacyRenderer_create(void) {
- GLLegacyRenderer* gl = safeCalloc(1, sizeof(GLLegacyRenderer));
- gl->base.vtable = &glVtable;
- gl->base.drawColor = 0xFFFFFF; // white (BGR)
- gl->base.drawAlpha = 1.0f;
- gl->base.drawFont = -1;
- gl->base.drawHalign = 0;
- gl->base.drawValign = 0;
- return (Renderer*) gl;
-}
diff --git a/src/glfw/gl_legacy_renderer.h b/src/glfw/gl_legacy_renderer.h
deleted file mode 100644
index 642b22d8..00000000
--- a/src/glfw/gl_legacy_renderer.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "renderer.h"
-#include
-
-// ===[ GLLegacyRenderer Struct ]===
-// Exposed in the header so platform-specific code (main.c) can access FBO fields for screenshots.
-typedef struct {
- Renderer base; // Must be first field for struct embedding
-
- GLuint* glTextures; // one GL texture per TXTR page
- int32_t* textureWidths; // needed for UV normalization
- int32_t* textureHeights;
- bool* textureLoaded; // lazy loading: true once PNG decoded and uploaded
- uint32_t textureCount;
-
- GLuint whiteTexture; // 1x1 white pixel for drawing primitives (rectangles, lines, etc.)
-
- int32_t windowW; // stored from beginFrame for endFrame blit
- int32_t windowH;
- int32_t gameW; // game resolution (for FBO sizing)
- int32_t gameH;
-
- // Original counts from data.win (dynamic slots start at these indices)
- uint32_t originalTexturePageCount;
- uint32_t originalTpagCount;
- uint32_t originalSpriteCount;
-} GLLegacyRenderer;
-
-Renderer* GLLegacyRenderer_create(void);
diff --git a/src/glfw/glfw_file_system.c b/src/glfw/glfw_file_system.c
deleted file mode 100644
index e66f11e7..00000000
--- a/src/glfw/glfw_file_system.c
+++ /dev/null
@@ -1,162 +0,0 @@
-#include "glfw_file_system.h"
-#include "utils.h"
-
-#include
-#include
-#include
-#include
-
-// ===[ Helpers ]===
-
-// Replaces all backslashes (\) with forward slashes (/) in a path string. (dealing with windows paths)
-static inline char* normalizePath(const char* path) {
- if (path == nullptr) return nullptr;
- char* normalized = safeStrdup(path);
- for (int i = 0; normalized[i] != '\0'; i++) {
- if (normalized[i] == '\\') {
- normalized[i] = '/';
- }
- }
- return normalized;
-}
-
-// The caller must make sure to free the returned string!
-static char* buildFullPath(GlfwFileSystem* fs, const char* relativePath) {
- char* normalizedPath = normalizePath(relativePath);
- if (strstr(normalizedPath, fs->basePath) != nullptr) return normalizedPath;
- size_t baseLen = strlen(fs->basePath);
- size_t relLen = strlen(normalizedPath);
- char* fullPath = safeMalloc(baseLen + relLen + 1);
- memcpy(fullPath, fs->basePath, baseLen);
- memcpy(fullPath + baseLen, normalizedPath, relLen);
- fullPath[baseLen + relLen] = '\0';
- free(normalizedPath);
- return fullPath;
-}
-
-// ===[ Vtable Implementations ]===
-
-// The caller must make sure to free the returned string!
-static char* glfwResolvePath(FileSystem* fs, const char* relativePath) {
- return buildFullPath((GlfwFileSystem*) fs, relativePath);
-}
-
-static bool glfwFileExists(FileSystem* fs, const char* relativePath) {
- char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath);
- struct stat st;
- bool exists = (stat(fullPath, &st) == 0);
- free(fullPath);
- return exists;
-}
-
-static char* glfwReadFileText(FileSystem* fs, const char* relativePath) {
- char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath);
- FILE* f = fopen(fullPath, "rb");
- free(fullPath);
- if (f == nullptr)
- return nullptr;
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- char* content = safeMalloc((size_t) size + 1);
- size_t bytesRead = fread(content, 1, (size_t) size, f);
- content[bytesRead] = '\0';
- fclose(f);
- return content;
-}
-
-static bool glfwWriteFileText(FileSystem* fs, const char* relativePath, const char* contents) {
- char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath);
- FILE* f = fopen(fullPath, "wb");
- free(fullPath);
- if (f == nullptr)
- return false;
-
- size_t len = strlen(contents);
- size_t written = fwrite(contents, 1, len, f);
- fclose(f);
- return written == len;
-}
-
-static bool glfwDeleteFile(FileSystem* fs, const char* relativePath) {
- char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath);
- int result = remove(fullPath);
- free(fullPath);
- return result == 0;
-}
-
-static bool glfwReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) {
- char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath);
- FILE* f = fopen(fullPath, "rb");
- free(fullPath);
- if (f == nullptr)
- return false;
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- uint8_t* data = safeMalloc((size_t) size);
- size_t bytesRead = fread(data, 1, (size_t) size, f);
- fclose(f);
-
- *outData = data;
- *outSize = (int32_t) bytesRead;
- return true;
-}
-
-static bool glfwWriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) {
- char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath);
- FILE* f = fopen(fullPath, "wb");
- free(fullPath);
- if (f == nullptr)
- return false;
-
- size_t written = fwrite(data, 1, (size_t) size, f);
- fclose(f);
- return written == (size_t) size;
-}
-
-// ===[ Vtable ]===
-
-static FileSystemVtable glfwFileSystemVtable = {
- .resolvePath = glfwResolvePath,
- .fileExists = glfwFileExists,
- .readFileText = glfwReadFileText,
- .writeFileText = glfwWriteFileText,
- .deleteFile = glfwDeleteFile,
- .readFileBinary = glfwReadFileBinary,
- .writeFileBinary = glfwWriteFileBinary,
-};
-
-// ===[ Lifecycle ]===
-
-GlfwFileSystem* GlfwFileSystem_create(const char* dataWinPath) {
- GlfwFileSystem* fs = safeCalloc(1, sizeof(GlfwFileSystem));
- fs->base.vtable = &glfwFileSystemVtable;
-
- // Derive basePath by stripping the filename from dataWinPath
- const char* lastSlash = strrchr(dataWinPath, '/');
- const char* lastBackslash = strrchr(dataWinPath, '\\');
- if (lastBackslash != nullptr && (lastSlash == nullptr || lastBackslash > lastSlash))
- lastSlash = lastBackslash;
- if (lastSlash != nullptr) {
- size_t dirLen = (size_t) (lastSlash - dataWinPath + 1); // include the trailing /
- fs->basePath = safeMalloc(dirLen + 1);
- memcpy(fs->basePath, dataWinPath, dirLen);
- fs->basePath[dirLen] = '\0';
- } else {
- // data.win is in current directory
- fs->basePath = safeStrdup("./");
- }
-
- return fs;
-}
-
-void GlfwFileSystem_destroy(GlfwFileSystem* fs) {
- if (fs == nullptr) return;
- free(fs->basePath);
- free(fs);
-}
diff --git a/src/glfw/glfw_file_system.h b/src/glfw/glfw_file_system.h
deleted file mode 100644
index addf2e63..00000000
--- a/src/glfw/glfw_file_system.h
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "file_system.h"
-
-typedef struct {
- FileSystem base;
- char* basePath; // directory containing data.win, with trailing separator
-} GlfwFileSystem;
-
-// Creates a GlfwFileSystem from the path to the data.win file
-// The basePath is derived by stripping the filename from dataWinPath.
-GlfwFileSystem* GlfwFileSystem_create(const char* dataWinPath);
-void GlfwFileSystem_destroy(GlfwFileSystem* fs);
diff --git a/src/glfw/glfw_gamepad.c b/src/glfw/glfw_gamepad.c
deleted file mode 100644
index 467902c0..00000000
--- a/src/glfw/glfw_gamepad.c
+++ /dev/null
@@ -1,142 +0,0 @@
-#include "glfw_gamepad.h"
-
-#include
-#include
-#include
-
-// ===[ Internal helpers ]===
-
-static float applyDeadzone(float value, float deadzone) {
- if (value < 0.0f) {
- if (value > -deadzone) return 0.0f;
- return (value + deadzone) / (1.0f - deadzone);
- } else {
- if (value < deadzone) return 0.0f;
- return (value - deadzone) / (1.0f - deadzone);
- }
-}
-
-enum {
- IDX_LT = 6,
- IDX_RT = 7,
-};
-
-static void mapGlfwToGml(const GLFWgamepadstate* glfwState, GamepadSlot* slot) {
- memcpy(slot->buttonDownPrev, slot->buttonDown, sizeof(slot->buttonDown));
- memset(slot->buttonDown, 0, sizeof(slot->buttonDown));
- memset(slot->buttonPressed, 0, sizeof(slot->buttonPressed));
- memset(slot->buttonReleased, 0, sizeof(slot->buttonReleased));
- memset(slot->buttonValue, 0, sizeof(slot->buttonValue));
- memset(slot->axisValue, 0, sizeof(slot->axisValue));
-
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_A]) slot->buttonDown[0] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_B]) slot->buttonDown[1] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_X]) slot->buttonDown[2] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_Y]) slot->buttonDown[3] = true;
-
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_LEFT_BUMPER]) slot->buttonDown[4] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER]) slot->buttonDown[5] = true;
-
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_BACK]) slot->buttonDown[8] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_START]) slot->buttonDown[9] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_GUIDE]) slot->buttonDown[16] = true;
-
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_LEFT_THUMB]) slot->buttonDown[10] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_RIGHT_THUMB]) slot->buttonDown[11] = true;
-
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_UP]) slot->buttonDown[12] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_DOWN]) slot->buttonDown[13] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_LEFT]) slot->buttonDown[14] = true;
- if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_RIGHT]) slot->buttonDown[15] = true;
-
- float lt = glfwState->axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER];
- float rt = glfwState->axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER];
- if (lt < 0.0f) lt = 0.0f;
- if (rt < 0.0f) rt = 0.0f;
- slot->buttonValue[IDX_LT] = lt;
- slot->buttonValue[IDX_RT] = rt;
- if (lt >= slot->triggerThreshold) slot->buttonDown[IDX_LT] = true;
- if (rt >= slot->triggerThreshold) slot->buttonDown[IDX_RT] = true;
-
- float lh = glfwState->axes[GLFW_GAMEPAD_AXIS_LEFT_X];
- float lv = glfwState->axes[GLFW_GAMEPAD_AXIS_LEFT_Y];
- float rh = glfwState->axes[GLFW_GAMEPAD_AXIS_RIGHT_X];
- float rv = glfwState->axes[GLFW_GAMEPAD_AXIS_RIGHT_Y];
-
- slot->axisValue[0] = applyDeadzone(lh, slot->deadzone);
- slot->axisValue[1] = applyDeadzone(lv, slot->deadzone);
- slot->axisValue[2] = applyDeadzone(rh, slot->deadzone);
- slot->axisValue[3] = applyDeadzone(rv, slot->deadzone);
-
- for (int i = 0; GP_BUTTON_COUNT > i; i++) {
- if (i == IDX_LT || i == IDX_RT) continue;
- slot->buttonValue[i] = slot->buttonDown[i] ? 1.0f : 0.0f;
- }
-}
-
-// ===[ Public API ]===
-
-void GlfwGamepad_loadMappings(const char* mappings) {
- if (mappings != NULL && mappings[0] != '\0') {
- if (glfwUpdateGamepadMappings(mappings)) {
- fprintf(stderr, "Gamepad: Loaded SDL gamecontroller mappings successfully\n");
- } else {
- fprintf(stderr, "Gamepad: Failed to load SDL gamecontroller mappings\n");
- }
- }
-}
-
-void GlfwGamepad_poll(RunnerGamepadState* gp) {
- for (int slotIdx = 0; slotIdx < 1 && slotIdx < MAX_GAMEPADS; slotIdx++) {
- GamepadSlot* slot = &gp->slots[slotIdx];
-
- bool currentlyConnected = false;
- int foundJid = -1;
-
- for (int jid = GLFW_JOYSTICK_1; jid <= GLFW_JOYSTICK_16; jid++) {
- if (glfwJoystickPresent(jid) && glfwJoystickIsGamepad(jid)) {
- foundJid = jid;
- currentlyConnected = true;
- break;
- }
- }
-
- if (currentlyConnected) {
- GLFWgamepadstate state;
- if (glfwGetGamepadState(foundJid, &state)) {
- mapGlfwToGml(&state, slot);
- slot->jid = foundJid;
- slot->connected = true;
-
- const char* name = glfwGetJoystickName(foundJid);
- if (name != NULL) {
- strncpy(slot->description, name, sizeof(slot->description) - 1);
- slot->description[sizeof(slot->description) - 1] = '\0';
- }
-
- const char* guid = glfwGetJoystickGUID(foundJid);
- if (guid != NULL) {
- strncpy(slot->guid, guid, sizeof(slot->guid) - 1);
- slot->guid[sizeof(slot->guid) - 1] = '\0';
- } else {
- slot->guid[0] = '\0';
- }
- } else {
- slot->connected = false;
- slot->guid[0] = '\0';
- }
- } else {
- slot->connected = false;
- slot->guid[0] = '\0';
- }
-
- if (slot->connected) {
- for (int btn = 0; GP_BUTTON_COUNT > btn; btn++) {
- bool wasDown = slot->buttonDownPrev[btn];
- if (slot->buttonDown[btn] && !wasDown) slot->buttonPressed[btn] = true;
- if (!slot->buttonDown[btn] && wasDown) slot->buttonReleased[btn] = true;
- }
- gp->connectedCount++;
- }
- }
-}
diff --git a/src/glfw/glfw_gamepad.h b/src/glfw/glfw_gamepad.h
deleted file mode 100644
index 48874288..00000000
--- a/src/glfw/glfw_gamepad.h
+++ /dev/null
@@ -1,8 +0,0 @@
-#pragma once
-
-#include "../runner_gamepad.h"
-
-// Loads SDL gamecontroller mappings into GLFW (call after glfwInit).
-void GlfwGamepad_loadMappings(const char* mappings);
-// Reads the physical joystick state from GLFW and updates RunnerGamepadState.
-void GlfwGamepad_poll(RunnerGamepadState* gp);
diff --git a/src/glfw/ma_audio_system.c b/src/glfw/ma_audio_system.c
deleted file mode 100644
index ea463886..00000000
--- a/src/glfw/ma_audio_system.c
+++ /dev/null
@@ -1,683 +0,0 @@
-// On Windows, include windows.h first so its headers are processed before stb_vorbis
-// defines single-letter macros (L, C, R) that conflict with winnt.h struct field names.
-#ifdef _WIN32
-#include
-#endif
-
-// Include stb_vorbis BEFORE miniaudio so that STB_VORBIS_INCLUDE_STB_VORBIS_H is defined,
-// which enables miniaudio's built-in OGG Vorbis decoding support.
-#include "stb_vorbis.c"
-
-#define MINIAUDIO_IMPLEMENTATION
-#include "miniaudio.h"
-
-#include "ma_audio_system.h"
-#include "data_win.h"
-#include "utils.h"
-
-#include
-#include
-#include
-#include "stb_ds.h"
-
-// ===[ Helpers ]===
-
-static SoundInstance* findFreeSlot(MaAudioSystem* ma) {
- // First pass: find an inactive slot
- repeat(MAX_SOUND_INSTANCES, i) {
- if (!ma->instances[i].active) {
- return &ma->instances[i];
- }
- }
-
- // Second pass: evict the lowest-priority ended sound
- SoundInstance* best = nullptr;
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (!ma_sound_is_playing(&inst->maSound)) {
- if (best == nullptr || best->priority > inst->priority) {
- best = inst;
- }
- }
- }
-
- if (best != nullptr) {
- ma_sound_uninit(&best->maSound);
- if (best->ownsDecoder) {
- ma_decoder_uninit(&best->decoder);
- }
- best->active = false;
- }
-
- return best;
-}
-
-static SoundInstance* findInstanceById(MaAudioSystem* ma, int32_t instanceId) {
- int32_t slotIndex = instanceId - SOUND_INSTANCE_ID_BASE;
- if (0 > slotIndex || slotIndex >= MAX_SOUND_INSTANCES) return nullptr;
- SoundInstance* inst = &ma->instances[slotIndex];
- if (!inst->active || inst->instanceId != instanceId) return nullptr;
- return inst;
-}
-
-// Helper: resolve external audio file path from Sound entry
-static char* resolveExternalPath(MaAudioSystem* ma, Sound* sound) {
- const char* file = sound->file;
- if (file == nullptr || file[0] == '\0') return nullptr;
-
- // If the filename has no extension, append ".ogg"
- bool hasExtension = (strchr(file, '.') != nullptr);
-
- char filename[512];
- if (hasExtension) {
- snprintf(filename, sizeof(filename), "%s", file);
- } else {
- snprintf(filename, sizeof(filename), "%s.ogg", file);
- }
-
- return ma->fileSystem->vtable->resolvePath(ma->fileSystem, filename);
-}
-
-// ===[ Vtable Implementations ]===
-
-static void maInit(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
- arrput(ma->base.audioGroups, dataWin);
- ma->fileSystem = fileSystem;
-
- ma_engine_config config = ma_engine_config_init();
- ma_result result = ma_engine_init(&config, &ma->engine);
- if (result != MA_SUCCESS) {
- fprintf(stderr, "Audio: Failed to initialize miniaudio engine (error %d)\n", result);
- return;
- }
-
- memset(ma->instances, 0, sizeof(ma->instances));
- ma->nextInstanceCounter = 0;
-
- fprintf(stderr, "Audio: miniaudio engine initialized\n");
-}
-
-static void maDestroy(AudioSystem* audio) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- // Uninit all active sound instances
- repeat(MAX_SOUND_INSTANCES, i) {
- if (ma->instances[i].active) {
- ma_sound_uninit(&ma->instances[i].maSound);
- if (ma->instances[i].ownsDecoder) {
- ma_decoder_uninit(&ma->instances[i].decoder);
- }
- ma->instances[i].active = false;
- }
- }
-
- // Free stream entries
- repeat(MAX_AUDIO_STREAMS, i) {
- if (ma->streams[i].active) {
- free(ma->streams[i].filePath);
- }
- }
-
- // Free loaded audio groups. The main data.win is owned by the caller, so skip index 0.
- if (arrlen(ma->base.audioGroups) > 1) {
- for (int32_t i = 1; i < (int32_t) arrlen(ma->base.audioGroups); i++) {
- DataWin_free(ma->base.audioGroups[i]);
- }
- }
- arrfree(ma->base.audioGroups);
-
- ma_engine_uninit(&ma->engine);
- free(ma);
-}
-
-static void maUpdate(AudioSystem* audio, float deltaTime) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (!inst->active) continue;
-
- // Handle gain fading (for cases where we do manual fading)
- if (inst->fadeTimeRemaining > 0.0f) {
- inst->fadeTimeRemaining -= deltaTime;
- if (0.0f >= inst->fadeTimeRemaining) {
- inst->fadeTimeRemaining = 0.0f;
- inst->currentGain = inst->targetGain;
- } else {
- float t = 1.0f - (inst->fadeTimeRemaining / inst->fadeTotalTime);
- inst->currentGain = inst->startGain + (inst->targetGain - inst->startGain) * t;
- }
- ma_sound_set_volume(&inst->maSound, inst->currentGain);
- }
-
- // Clean up ended non-looping sounds (ma_sound_at_end avoids reaping still-loading async sounds)
- if (ma_sound_at_end(&inst->maSound) && !ma_sound_is_looping(&inst->maSound)) {
- ma_sound_uninit(&inst->maSound);
- if (inst->ownsDecoder) {
- ma_decoder_uninit(&inst->decoder);
- }
- inst->active = false;
- }
- }
-}
-
-static int32_t maPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- // Check if this is a stream index (created by audio_create_stream)
- bool isStream = (soundIndex >= AUDIO_STREAM_INDEX_BASE);
- Sound* sound = nullptr;
- char* streamPath = nullptr;
-
- if (isStream) {
- int32_t streamSlot = soundIndex - AUDIO_STREAM_INDEX_BASE;
- if (0 > streamSlot || streamSlot >= MAX_AUDIO_STREAMS || !ma->streams[streamSlot].active) {
- fprintf(stderr, "Audio: Invalid stream index %d\n", soundIndex);
- return -1;
- }
- streamPath = ma->streams[streamSlot].filePath;
- } else {
- DataWin* dw = ma->base.audioGroups[0]; // Audio Group 0 should always be data.win
- if (0 > soundIndex || (uint32_t) soundIndex >= dw->sond.count) {
- fprintf(stderr, "Audio: Invalid sound index %d\n", soundIndex);
- return -1;
- }
- sound = &dw->sond.sounds[soundIndex];
- }
-
- SoundInstance* slot = findFreeSlot(ma);
- if (slot == nullptr) {
- fprintf(stderr, "Audio: No free sound slots for sound %d\n", soundIndex);
- return -1;
- }
-
- int32_t slotIndex = (int32_t) (slot - ma->instances);
- ma_result result;
-
- if (isStream) {
- // Stream audio: load from file path stored in stream entry
- result = ma_sound_init_from_file(&ma->engine, streamPath, MA_SOUND_FLAG_ASYNC, nullptr, nullptr, &slot->maSound);
- if (result != MA_SUCCESS) {
- fprintf(stderr, "Audio: Failed to load stream file '%s' (error %d)\n", streamPath, result);
- return -1;
- }
- slot->ownsDecoder = false;
- } else {
- bool isEmbedded = (sound->flags & 0x01) != 0;
- bool isCompressed = (sound->flags & 0x02) != 0;
-
- if (isEmbedded || isCompressed) {
- // Embedded audio: decode from AUDO chunk memory
- if (0 > sound->audioFile || (uint32_t) sound->audioFile >= ma->base.audioGroups[sound->audioGroup]->audo.count) {
- fprintf(stderr, "Audio: Invalid audio file index %d for sound '%s'\n", sound->audioFile, sound->name);
- return -1;
- }
-
- AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile];
-
- ma_decoder_config decoderConfig = ma_decoder_config_init_default();
- result = ma_decoder_init_memory(entry->data, entry->dataSize, &decoderConfig, &slot->decoder);
- if (result != MA_SUCCESS) {
- fprintf(stderr, "Audio: Failed to init decoder for '%s' (error %d)\n", sound->name, result);
- return -1;
- }
- slot->ownsDecoder = true;
-
- result = ma_sound_init_from_data_source(&ma->engine, &slot->decoder, 0, nullptr, &slot->maSound);
- if (result != MA_SUCCESS) {
- fprintf(stderr, "Audio: Failed to init sound from decoder for '%s' (error %d)\n", sound->name, result);
- ma_decoder_uninit(&slot->decoder);
- return -1;
- }
- } else {
- // External audio: load from file
- char* path = resolveExternalPath(ma, sound);
- if (path == nullptr) {
- fprintf(stderr, "Audio: Could not resolve path for sound '%s'\n", sound->name);
- return -1;
- }
-
- result = ma_sound_init_from_file(&ma->engine, path, MA_SOUND_FLAG_ASYNC, nullptr, nullptr, &slot->maSound);
- if (result != MA_SUCCESS) {
- fprintf(stderr, "Audio: Failed to load file for '%s' at '%s' (error %d)\n", sound->name, path, result);
- free(path);
- return -1;
- }
- free(path);
- slot->ownsDecoder = false;
- }
- }
-
- // Apply properties
- float volume = isStream ? 1.0f : sound->volume;
- float pitch = isStream ? 1.0f : sound->pitch;
- ma_sound_set_volume(&slot->maSound, volume);
- if (pitch != 1.0f) {
- ma_sound_set_pitch(&slot->maSound, pitch);
- }
- ma_sound_set_looping(&slot->maSound, loop);
-
- // Set up instance tracking
- slot->active = true;
- slot->soundIndex = soundIndex;
- slot->instanceId = SOUND_INSTANCE_ID_BASE + slotIndex;
- slot->currentGain = volume;
- slot->targetGain = volume;
- slot->fadeTimeRemaining = 0.0f;
- slot->fadeTotalTime = 0.0f;
- slot->startGain = volume;
- slot->priority = priority;
-
- // Track unique IDs for disambiguation
- ma->nextInstanceCounter++;
-
- ma_sound_start(&slot->maSound);
-
- return slot->instanceId;
-}
-
-static void maStopSound(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- // Stop specific instance
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) {
- ma_sound_stop(&inst->maSound);
- ma_sound_uninit(&inst->maSound);
- if (inst->ownsDecoder) {
- ma_decoder_uninit(&inst->decoder);
- }
- inst->active = false;
- }
- } else {
- // Stop all instances of this sound resource
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- ma_sound_stop(&inst->maSound);
- ma_sound_uninit(&inst->maSound);
- if (inst->ownsDecoder) {
- ma_decoder_uninit(&inst->decoder);
- }
- inst->active = false;
- }
- }
- }
-}
-
-static void maStopAll(AudioSystem* audio) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active) {
- ma_sound_stop(&inst->maSound);
- ma_sound_uninit(&inst->maSound);
- if (inst->ownsDecoder) {
- ma_decoder_uninit(&inst->decoder);
- }
- inst->active = false;
- }
- }
-}
-
-static bool maIsPlaying(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- return inst != nullptr && ma_sound_is_playing(&inst->maSound);
- } else {
- // Check if any instance of this sound resource is playing
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance && ma_sound_is_playing(&inst->maSound)) {
- return true;
- }
- }
- return false;
- }
-}
-
-static void maPauseSound(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) {
- ma_sound_stop(&inst->maSound);
- }
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- ma_sound_stop(&inst->maSound);
- }
- }
- }
-}
-
-static void maResumeSound(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) {
- ma_sound_start(&inst->maSound);
- }
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- ma_sound_start(&inst->maSound);
- }
- }
- }
-}
-
-static void maPauseAll(AudioSystem* audio) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && ma_sound_is_playing(&inst->maSound)) {
- ma_sound_stop(&inst->maSound);
- }
- }
-}
-
-static void maResumeAll(AudioSystem* audio) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active) {
- ma_sound_start(&inst->maSound);
- }
- }
-}
-
-static void maSetSoundGain(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) {
- if (timeMs == 0) {
- inst->currentGain = gain;
- inst->targetGain = gain;
- inst->fadeTimeRemaining = 0.0f;
- ma_sound_set_volume(&inst->maSound, gain);
- } else {
- inst->startGain = inst->currentGain;
- inst->targetGain = gain;
- inst->fadeTotalTime = (float) timeMs / 1000.0f;
- inst->fadeTimeRemaining = inst->fadeTotalTime;
- }
- }
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- if (timeMs == 0) {
- inst->currentGain = gain;
- inst->targetGain = gain;
- inst->fadeTimeRemaining = 0.0f;
- ma_sound_set_volume(&inst->maSound, gain);
- } else {
- inst->startGain = inst->currentGain;
- inst->targetGain = gain;
- inst->fadeTotalTime = (float) timeMs / 1000.0f;
- inst->fadeTimeRemaining = inst->fadeTotalTime;
- }
- }
- }
- }
-}
-
-static float maGetSoundGain(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) return inst->currentGain;
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- return inst->currentGain;
- }
- }
- }
- return 0.0f;
-}
-
-static void maSetSoundPitch(AudioSystem* audio, int32_t soundOrInstance, float pitch) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) {
- ma_sound_set_pitch(&inst->maSound, pitch);
- }
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- ma_sound_set_pitch(&inst->maSound, pitch);
- }
- }
- }
-}
-
-static float maGetSoundPitch(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) return ma_sound_get_pitch(&inst->maSound);
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- return ma_sound_get_pitch(&inst->maSound);
- }
- }
- }
- return 1.0f;
-}
-
-static float maGetTrackPosition(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) {
- float cursor;
- ma_result result = ma_sound_get_cursor_in_seconds(&inst->maSound, &cursor);
- if (result == MA_SUCCESS) return cursor;
- }
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- float cursor;
- ma_result result = ma_sound_get_cursor_in_seconds(&inst->maSound, &cursor);
- if (result == MA_SUCCESS) return cursor;
- }
- }
- }
- return 0.0f;
-}
-
-static void maSetTrackPosition(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- SoundInstance* inst = findInstanceById(ma, soundOrInstance);
- if (inst != nullptr) {
- ma_sound_seek_to_pcm_frame(&inst->maSound, (ma_uint64) (positionSeconds * 44100.0f));
- }
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- ma_sound_seek_to_pcm_frame(&inst->maSound, (ma_uint64) (positionSeconds * 44100.0f));
- }
- }
- }
-}
-
-// Total length of a loaded sound. Works on both SOND index and active instance ids.
-// Uses miniaudio's ma_sound_get_length_in_seconds, which reads the decoded duration from the underlying data source (works for fully-decoded sounds AND streaming sounds).
-static float maGetSoundLength(AudioSystem* audio, int32_t soundOrInstance) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- SoundInstance* match = nullptr;
- if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) {
- match = findInstanceById(ma, soundOrInstance);
- } else {
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance) {
- match = inst;
- break;
- }
- }
- }
- if (match == nullptr) return 0.0f;
-
- float seconds = 0.0f;
- if (ma_sound_get_length_in_seconds(&match->maSound, &seconds) != MA_SUCCESS) return 0.0f;
- return seconds;
-}
-
-static void maSetMasterGain(AudioSystem* audio, float gain) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
- ma_engine_set_volume(&ma->engine, gain);
-}
-
-static void maSetChannelCount(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t count) {
- // miniaudio handles channel management internally, this is a no-op
-}
-
-static void maGroupLoad(AudioSystem* audio, int32_t groupIndex) {
- if (groupIndex > 0) {
- int sz = snprintf(nullptr, 0, "audiogroup%d.dat", groupIndex);
- char buf[sz + 1];
- snprintf(buf, sizeof(buf), "audiogroup%d.dat", groupIndex);
- DataWin *audioGroup = DataWin_parse(((MaAudioSystem*)audio)->fileSystem->vtable->resolvePath(((MaAudioSystem*)audio)->fileSystem, buf),
- (DataWinParserOptions) {
- .parseAudo = true,
- });
- arrput(audio->audioGroups, audioGroup);
- }
-}
-
-static bool maGroupIsLoaded(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t groupIndex) {
- return (arrlen(audio->audioGroups) > groupIndex);
-}
-
-// ===[ Audio Streams ]===
-
-static int32_t maCreateStream(AudioSystem* audio, const char* filename) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- // Find a free stream slot
- int32_t freeSlot = -1;
- repeat(MAX_AUDIO_STREAMS, i) {
- if (!ma->streams[i].active) {
- freeSlot = (int32_t) i;
- break;
- }
- }
-
- if (0 > freeSlot) {
- fprintf(stderr, "Audio: No free stream slots for '%s'\n", filename);
- return -1;
- }
-
- char* resolved = ma->fileSystem->vtable->resolvePath(ma->fileSystem, filename);
- if (resolved == nullptr) {
- fprintf(stderr, "Audio: Could not resolve path for stream '%s'\n", filename);
- return -1;
- }
-
- ma->streams[freeSlot].active = true;
- ma->streams[freeSlot].filePath = resolved;
-
- int32_t streamIndex = AUDIO_STREAM_INDEX_BASE + freeSlot;
- fprintf(stderr, "Audio: Created stream %d for '%s' -> '%s'\n", streamIndex, filename, resolved);
- return streamIndex;
-}
-
-static bool maDestroyStream(AudioSystem* audio, int32_t streamIndex) {
- MaAudioSystem* ma = (MaAudioSystem*) audio;
-
- int32_t slotIndex = streamIndex - AUDIO_STREAM_INDEX_BASE;
- if (0 > slotIndex || slotIndex >= MAX_AUDIO_STREAMS) {
- fprintf(stderr, "Audio: Invalid stream index %d for destroy\n", streamIndex);
- return false;
- }
-
- AudioStreamEntry* entry = &ma->streams[slotIndex];
- if (!entry->active) return false;
-
- // Stop all sound instances that were playing this stream
- repeat(MAX_SOUND_INSTANCES, i) {
- SoundInstance* inst = &ma->instances[i];
- if (inst->active && inst->soundIndex == streamIndex) {
- ma_sound_stop(&inst->maSound);
- ma_sound_uninit(&inst->maSound);
- if (inst->ownsDecoder) {
- ma_decoder_uninit(&inst->decoder);
- }
- inst->active = false;
- }
- }
-
- free(entry->filePath);
- entry->filePath = nullptr;
- entry->active = false;
- fprintf(stderr, "Audio: Destroyed stream %d\n", streamIndex);
- return true;
-}
-
-// ===[ Vtable ]===
-
-static AudioSystemVtable maAudioSystemVtable = {
- .init = maInit,
- .destroy = maDestroy,
- .update = maUpdate,
- .playSound = maPlaySound,
- .stopSound = maStopSound,
- .stopAll = maStopAll,
- .isPlaying = maIsPlaying,
- .pauseSound = maPauseSound,
- .resumeSound = maResumeSound,
- .pauseAll = maPauseAll,
- .resumeAll = maResumeAll,
- .setSoundGain = maSetSoundGain,
- .getSoundGain = maGetSoundGain,
- .setSoundPitch = maSetSoundPitch,
- .getSoundPitch = maGetSoundPitch,
- .getTrackPosition = maGetTrackPosition,
- .setTrackPosition = maSetTrackPosition,
- .getSoundLength = maGetSoundLength,
- .setMasterGain = maSetMasterGain,
- .setChannelCount = maSetChannelCount,
- .groupLoad = maGroupLoad,
- .groupIsLoaded = maGroupIsLoaded,
- .createStream = maCreateStream,
- .destroyStream = maDestroyStream,
-};
-
-// ===[ Lifecycle ]===
-
-MaAudioSystem* MaAudioSystem_create(void) {
- MaAudioSystem* ma = safeCalloc(1, sizeof(MaAudioSystem));
- ma->base.vtable = &maAudioSystemVtable;
- return ma;
-}
diff --git a/src/glfw/ma_audio_system.h b/src/glfw/ma_audio_system.h
deleted file mode 100644
index 627c75d4..00000000
--- a/src/glfw/ma_audio_system.h
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "audio_system.h"
-#include "miniaudio.h"
-
-#define MAX_SOUND_INSTANCES 128
-#define SOUND_INSTANCE_ID_BASE 100000
-#define MAX_AUDIO_STREAMS 32
-// This is the index space that the native runner uses
-#define AUDIO_STREAM_INDEX_BASE 300000
-
-typedef struct {
- bool active;
- int32_t soundIndex; // SOND resource that spawned this
- int32_t instanceId; // unique ID returned to GML
- ma_sound maSound; // miniaudio sound object
- ma_decoder decoder; // decoder for memory-based audio
- bool ownsDecoder; // true if decoder needs uninit
- float targetGain;
- float currentGain;
- float fadeTimeRemaining;
- float fadeTotalTime;
- float startGain;
- int32_t priority;
-} SoundInstance;
-
-typedef struct {
- bool active;
- char* filePath; // resolved file path (owned, freed on destroy)
-} AudioStreamEntry;
-
-typedef struct {
- AudioSystem base;
- ma_engine engine;
- SoundInstance instances[MAX_SOUND_INSTANCES];
- int32_t nextInstanceCounter;
- FileSystem* fileSystem;
- AudioStreamEntry streams[MAX_AUDIO_STREAMS];
-} MaAudioSystem;
-
-MaAudioSystem* MaAudioSystem_create(void);
diff --git a/src/glfw/main.c b/src/glfw/main.c
deleted file mode 100644
index e8bce9c3..00000000
--- a/src/glfw/main.c
+++ /dev/null
@@ -1,1197 +0,0 @@
-#include "data_win.h"
-#include "glfw/gl_legacy_renderer.h"
-#include "vm.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#ifdef _WIN32
-#include
-#endif
-#ifdef __GLIBC__
-#include
-#endif
-
-#include "runner_keyboard.h"
-#include "glfw_gamepad.h"
-#include "runner.h"
-#include "input_recording.h"
-#include "debug_overlay.h"
-#include "gl_renderer.h"
-#include "glfw_file_system.h"
-#include "ma_audio_system.h"
-#include "noop_audio_system.h"
-#include "stb_ds.h"
-#include "stb_image_write.h"
-
-#include "utils.h"
-#include "profiler.h"
-
-static void glfwErrorCallback(int code, const char* description) {
- fprintf(stderr, "GLFW error 0x%x: %s\n", code, description);
-}
-
-#ifndef ENABLE_GLES
-static void APIENTRY glDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, MAYBE_UNUSED GLsizei length, const GLchar* message, MAYBE_UNUSED const void* userParam) {
- const char* sourceStr;
- switch (source) {
- case GL_DEBUG_SOURCE_API: sourceStr = "API"; break;
- case GL_DEBUG_SOURCE_WINDOW_SYSTEM: sourceStr = "Window System"; break;
- case GL_DEBUG_SOURCE_SHADER_COMPILER: sourceStr = "Shader Compiler"; break;
- case GL_DEBUG_SOURCE_THIRD_PARTY: sourceStr = "Third Party"; break;
- case GL_DEBUG_SOURCE_APPLICATION: sourceStr = "Application"; break;
- case GL_DEBUG_SOURCE_OTHER: sourceStr = "Other"; break;
- default: sourceStr = "Unknown"; break;
- }
-
- const char* typeStr;
- switch (type) {
- case GL_DEBUG_TYPE_ERROR: typeStr = "Error"; break;
- case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: typeStr = "Deprecated Behaviour"; break;
- case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: typeStr = "Undefined Behaviour"; break;
- case GL_DEBUG_TYPE_PORTABILITY: typeStr = "Portability"; break;
- case GL_DEBUG_TYPE_PERFORMANCE: typeStr = "Performance"; break;
- case GL_DEBUG_TYPE_MARKER: typeStr = "Marker"; break;
- case GL_DEBUG_TYPE_PUSH_GROUP: typeStr = "Push Group"; break;
- case GL_DEBUG_TYPE_POP_GROUP: typeStr = "Pop Group"; break;
- case GL_DEBUG_TYPE_OTHER: typeStr = "Other"; break;
- default: typeStr = "Unknown"; break;
- }
-
- const char* severityStr;
- switch (severity) {
- case GL_DEBUG_SEVERITY_HIGH: severityStr = "High"; break;
- case GL_DEBUG_SEVERITY_MEDIUM: severityStr = "Medium"; break;
- case GL_DEBUG_SEVERITY_LOW: severityStr = "Low"; break;
- case GL_DEBUG_SEVERITY_NOTIFICATION: severityStr = "Notification"; break;
- default: severityStr = "Unknown"; break;
- }
-
- fprintf(stderr, "[OpenGL %s] id=%u Type: %s; Severity: %s; Message: %.*s\n", sourceStr, id, typeStr, severityStr, (int) length, message);
-}
-
-static void installGLDebugCallback(void) {
- if (!GLAD_GL_KHR_debug) {
- fprintf(stderr, "OpenGL debug callback not available (driver does not expose GL_KHR_debug)\n");
- return;
- }
-
- glEnable(GL_DEBUG_OUTPUT);
- glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
- glDebugMessageCallbackKHR(glDebugCallback, nullptr);
- glDebugMessageControlKHR(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);
-}
-#endif
-
-// ===[ COMMAND LINE ARGUMENTS ]===
-typedef struct {
- int key;
- // We need this dummy value, think that the ds_map is like a Java HashMap NOT a HashSet
- // (Which is funny, because in Java HashSets are backed by HashMaps lol)
- bool value;
-} FrameSetEntry;
-
-typedef struct {
- const char* dataWinPath;
- const char* screenshotPattern;
- FrameSetEntry* screenshotFrames;
- FrameSetEntry* dumpFrames;
- FrameSetEntry* dumpJsonFrames;
- const char* dumpJsonFilePattern;
- StringBooleanEntry* varReadsToBeTraced;
- StringBooleanEntry* varWritesToBeTraced;
- StringBooleanEntry* functionCallsToBeTraced;
- StringBooleanEntry* alarmsToBeTraced;
- StringBooleanEntry* instanceLifecyclesToBeTraced;
- StringBooleanEntry* eventsToBeTraced;
- StringBooleanEntry* opcodesToBeTraced;
- StringBooleanEntry* stackToBeTraced;
- StringBooleanEntry* disassemble;
- StringBooleanEntry* tilesToBeTraced;
- bool alwaysLogUnknownFunctions;
- bool alwaysLogStubbedFunctions;
- bool headless;
- bool traceFrames;
- bool printRooms;
- bool printDeclaredFunctions;
- int exitAtFrame;
- int traceBytecodeAfterFrame;
- double speedMultiplier;
- double fastForwardSpeed;
- int seed;
- bool hasSeed;
- bool debug;
- bool traceEventInherited;
- const char* recordInputsPath;
- const char* playbackInputsPath;
- const char* renderer;
- YoYoOperatingSystem osType;
- bool lazyRooms;
- StringBooleanEntry* eagerRooms; // stb_ds string-keyed set of room names
- int profilerFramesBetween; // 0 = disabled
-#ifdef ENABLE_VM_OPCODE_PROFILER
- bool opcodeProfiler;
-#endif
-} CommandLineArgs;
-
-typedef struct { const char* name; YoYoOperatingSystem value; } OsTypeNameEntry;
-
-static const OsTypeNameEntry OS_TYPE_NAMES[] = {
- {"unknown", OS_UNKNOWN},
- {"windows", OS_WINDOWS},
- {"win32", OS_WINDOWS},
- {"macosx", OS_MACOSX},
- {"macos", OS_MACOSX},
- {"psp", OS_PSP},
- {"ios", OS_IOS},
- {"android", OS_ANDROID},
- {"symbian", OS_SYMBIAN},
- {"linux", OS_LINUX},
- {"winphone", OS_WINPHONE},
- {"tizen", OS_TIZEN},
- {"win8native", OS_WIN8NATIVE},
- {"wiiu", OS_WIIU},
- {"3ds", OS_3DS},
- {"psvita", OS_PSVITA},
- {"bb10", OS_BB10},
- {"ps4", OS_PS4},
- {"xboxone", OS_XBOXONE},
- {"ps3", OS_PS3},
- {"xbox360", OS_XBOX360},
- {"uwp", OS_UWP},
- {"amazon", OS_AMAZON},
- {"switch", OS_SWITCH},
-};
-#define OS_TYPE_NAMES_COUNT (sizeof(OS_TYPE_NAMES)/sizeof(OS_TYPE_NAMES[0]))
-
-static bool parseOsTypeArg(const char* s, YoYoOperatingSystem* out) {
- forEach(const OsTypeNameEntry, entry, OS_TYPE_NAMES, OS_TYPE_NAMES_COUNT) {
- if (strcmp(s, entry->name) == 0) {
- *out = entry->value;
- return true;
- }
- }
- return false;
-}
-
-static void printOsTypeNames(FILE* out) {
- forEachIndexed(const OsTypeNameEntry, entry, i, OS_TYPE_NAMES, OS_TYPE_NAMES_COUNT) {
- fprintf(out, "%s%s", i > 0 ? ", " : "", entry->name);
- }
-}
-
-static void parseCommandLineArgs(CommandLineArgs* args, int argc, char* argv[]) {
- memset(args, 0, sizeof(CommandLineArgs));
-
- static struct option longOptions[] = {
- {"screenshot", required_argument, nullptr, 's'},
- {"screenshot-at-frame", required_argument, nullptr, 'f'},
- {"headless", no_argument, nullptr, 'h'},
- {"print-rooms", no_argument, nullptr, 'r'},
- {"print-declared-functions", no_argument, nullptr, 'p'},
- {"trace-variable-reads", required_argument, nullptr, 'R'},
- {"trace-variable-writes", required_argument, nullptr, 'W'},
- {"trace-function-calls", required_argument, nullptr, 'c'},
- {"trace-alarms", required_argument, nullptr, 'a'},
- {"trace-instance-lifecycles", required_argument, nullptr, 'l'},
- {"trace-events", required_argument, nullptr, 'e'},
- {"trace-event-inherited", no_argument, nullptr, 'E'},
- {"trace-tiles", required_argument, nullptr, 'T'},
- {"trace-opcodes", required_argument, nullptr, 'o'},
- {"trace-stack", required_argument, nullptr, 'S'},
- {"trace-frames", no_argument, nullptr, 'k'},
- {"always-log-unknown-functions", no_argument, nullptr, 'y'},
- {"always-log-stubbed-functions", no_argument, nullptr, 'Y'},
- {"exit-at-frame", required_argument, nullptr, 'x'},
- {"trace-bytecode-after-frame", required_argument, nullptr, 'F'},
- {"dump-frame", required_argument, nullptr, 'd'},
- {"dump-frame-json", required_argument, nullptr, 'j'},
- {"dump-frame-json-file", required_argument, nullptr, 'J'},
- {"speed", required_argument, nullptr, 'M'},
- {"fast-forward-speed", required_argument, nullptr, 'X'},
- {"seed", required_argument, nullptr, 'Z'},
- {"debug", no_argument, nullptr, 'D'},
- {"disassemble", required_argument, nullptr, 'A'},
- {"record-inputs", required_argument, nullptr, 'I'},
- {"playback-inputs", required_argument, nullptr, 'P'},
- {"renderer", required_argument, nullptr, 'g'},
- {"lazy-rooms", no_argument, nullptr, 'z'},
- {"eager-room", required_argument, nullptr, 'G'},
- {"os-type", required_argument, nullptr, 'O'},
- {"profile-gml-scripts", required_argument, nullptr, 'q'},
-#ifdef ENABLE_VM_OPCODE_PROFILER
- {"profile-opcodes", no_argument, nullptr, 'Q'},
-#endif
- {nullptr, 0, nullptr, 0 }
- };
-
- args->screenshotFrames = nullptr;
- args->exitAtFrame = -1;
- args->traceBytecodeAfterFrame = 0;
- args->speedMultiplier = 1.0;
- args->fastForwardSpeed = 0.0;
- args->renderer = "gl";
- args->osType = OS_WINDOWS;
- args->profilerFramesBetween = 0;
-
- int opt;
- while ((opt = getopt_long(argc, argv, "", longOptions, nullptr)) != -1) {
- switch (opt) {
- case 's':
- args->screenshotPattern = optarg;
- break;
- case 'f': {
- char* endPtr;
- long frame = strtol(optarg, &endPtr, 10);
- if (*endPtr != '\0' || 0 > frame) {
- fprintf(stderr, "Error: Invalid frame number '%s'\n", optarg);
- exit(1);
- }
-
- hmput(args->screenshotFrames, (int) frame, true);
- break;
- }
- case 'h':
- args->headless = true;
- break;
- case 'r':
- args->printRooms = true;
- break;
- case 'p':
- args->printDeclaredFunctions = true;
- break;
- case 'R':
- shput(args->varReadsToBeTraced, optarg, true);
- break;
- case 'W':
- shput(args->varWritesToBeTraced, optarg, true);
- break;
- case 'c':
- shput(args->functionCallsToBeTraced, optarg, true);
- break;
- case 'a':
- shput(args->alarmsToBeTraced, optarg, true);
- break;
- case 'l':
- shput(args->instanceLifecyclesToBeTraced, optarg, true);
- break;
- case 'e':
- shput(args->eventsToBeTraced, optarg, true);
- break;
- case 'o':
- shput(args->opcodesToBeTraced, optarg, true);
- break;
- case 'S':
- shput(args->stackToBeTraced, optarg, true);
- break;
- case 'k':
- args->traceFrames = true;
- break;
- case 'y':
- args->alwaysLogUnknownFunctions = true;
- break;
- case 'Y':
- args->alwaysLogStubbedFunctions = true;
- break;
- case 'x': {
- char* endPtr;
- long frame = strtol(optarg, &endPtr, 10);
- if (*endPtr != '\0' || 0 > frame) {
- fprintf(stderr, "Error: Invalid frame number '%s' for --exit-at-frame\n", optarg);
- exit(1);
- }
- args->exitAtFrame = (int) frame;
- break;
- }
- case 'F': {
- char* endPtr;
- long frame = strtol(optarg, &endPtr, 10);
- if (*endPtr != '\0' || 0 > frame) {
- fprintf(stderr, "Error: Invalid frame number '%s' for --trace-bytecode-after-frame\n", optarg);
- exit(1);
- }
- args->traceBytecodeAfterFrame = (int) frame;
- break;
- }
- case 'd': {
- char* endPtr;
- long frame = strtol(optarg, &endPtr, 10);
- if (*endPtr != '\0' || 0 > frame) {
- fprintf(stderr, "Error: Invalid frame number '%s' for --dump-frame\n", optarg);
- exit(1);
- }
- hmput(args->dumpFrames, (int) frame, true);
- break;
- }
- case 'j': {
- char* endPtr;
- long frame = strtol(optarg, &endPtr, 10);
- if (*endPtr != '\0' || 0 > frame) {
- fprintf(stderr, "Error: Invalid frame number '%s' for --dump-frame-json\n", optarg);
- exit(1);
- }
- hmput(args->dumpJsonFrames, (int) frame, true);
- break;
- }
- case 'J':
- args->dumpJsonFilePattern = optarg;
- break;
- case 'M': {
- char* endPtr;
- double speed = strtod(optarg, &endPtr);
- if (*endPtr != '\0' || speed <= 0.0) {
- fprintf(stderr, "Error: Invalid speed multiplier '%s' for --speed (must be > 0)\n", optarg);
- exit(1);
- }
- args->speedMultiplier = speed;
- break;
- }
- case 'X': {
- char* endPtr;
- double speed = strtod(optarg, &endPtr);
- if (*endPtr != '\0' || speed <= 0.0) {
- fprintf(stderr, "Error: Invalid speed '%s' for --fast-forward-speed (must be > 0)\n", optarg);
- exit(1);
- }
- args->fastForwardSpeed = speed;
- break;
- }
- case 'D':
- args->debug = true;
- break;
- case 'g':
- args->renderer = optarg;
- break;
- case 'z':
- args->lazyRooms = true;
- break;
- case 'G':
- shput(args->eagerRooms, optarg, true);
- break;
- case 'A':
- shput(args->disassemble, optarg, true);
- break;
- case 'T':
- shput(args->tilesToBeTraced, optarg, true);
- break;
- case 'E':
- args->traceEventInherited = true;
- break;
- case 'Z': {
- char* endPtr;
- long seedVal = strtol(optarg, &endPtr, 10);
- if (*endPtr != '\0') {
- fprintf(stderr, "Error: Invalid seed value '%s' for --seed\n", optarg);
- exit(1);
- }
- args->seed = (int) seedVal;
- args->hasSeed = true;
- break;
- }
- case 'I':
- args->recordInputsPath = optarg;
- break;
- case 'P':
- args->playbackInputsPath = optarg;
- break;
- case 'q': {
- char* endPtr;
- long framesBetween = strtol(optarg, &endPtr, 10);
- if (*endPtr != '\0' || framesBetween <= 0) {
- fprintf(stderr, "Error: Invalid frame count '%s' for --profile-gml-scripts (must be > 0)\n", optarg);
- exit(1);
- }
- args->profilerFramesBetween = (int) framesBetween;
- break;
- }
-#ifdef ENABLE_VM_OPCODE_PROFILER
- case 'Q':
- args->opcodeProfiler = true;
- break;
-#endif
- case 'O':
- if (!parseOsTypeArg(optarg, &args->osType)) {
- fprintf(stderr, "Error: Invalid --os-type value '%s' (expected: ", optarg);
- printOsTypeNames(stderr);
- fprintf(stderr, ")\n");
- exit(1);
- }
- break;
- default:
- fprintf(stderr, "Usage: %s [--headless] [--screenshot=PATTERN] [--screenshot-at-frame=N ...] \n", argv[0]);
- exit(1);
- }
- }
-
- if (optind >= argc) {
- fprintf(stderr, "Usage: %s [--headless] [--screenshot=PATTERN] [--screenshot-at-frame=N ...] \n", argv[0]);
- exit(1);
- }
-
- args->dataWinPath = argv[optind];
-
- if (hmlen(args->screenshotFrames) > 0 && args->screenshotPattern == nullptr) {
- fprintf(stderr, "Error: --screenshot-at-frame requires --screenshot to be set\n");
- exit(1);
- }
-
- if (args->headless && args->speedMultiplier != 1.0) {
- fprintf(stderr, "You can't set the speed multiplier while running in headless mode! Headless mode always run in real time\n");
- exit(1);
- }
-
-}
-
-static void freeCommandLineArgs(CommandLineArgs* args) {
- hmfree(args->screenshotFrames);
- hmfree(args->dumpFrames);
- hmfree(args->dumpJsonFrames);
- shfree(args->varReadsToBeTraced);
- shfree(args->varWritesToBeTraced);
- shfree(args->functionCallsToBeTraced);
- shfree(args->alarmsToBeTraced);
- shfree(args->instanceLifecyclesToBeTraced);
- shfree(args->eventsToBeTraced);
- shfree(args->opcodesToBeTraced);
- shfree(args->stackToBeTraced);
- shfree(args->disassemble);
- shfree(args->tilesToBeTraced);
-}
-
-// ===[ SCREENSHOT ]===
-static void captureScreenshot(const char* filenamePattern, int frameNumber, int width, int height) {
- char filename[512];
- snprintf(filename, sizeof(filename), filenamePattern, frameNumber);
-
- int stride = width * 4;
- unsigned char* pixels = safeMalloc(stride * height);
- if (pixels == nullptr) {
- fprintf(stderr, "Error: Failed to allocate memory for screenshot (%dx%d)\n", width, height);
- return;
- }
-
- glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
-
- // OpenGL reads bottom-to-top, but PNG is top-to-bottom.
- // Use stb's negative stride trick: point to the last row and use a negative stride to flip vertically.
- unsigned char* lastRow = pixels + (height - 1) * stride;
- stbi_write_png(filename, width, height, 4, lastRow, -stride);
-
- free(pixels);
- printf("Screenshot saved: %s\n", filename);
-}
-
-// ===[ KEYBOARD INPUT ]===
-
-static int32_t glfwKeyToGml(int glfwKey) {
- // Letters: GLFW_KEY_A (65) -> 65 (same as GML)
- if (glfwKey >= GLFW_KEY_A && glfwKey <= GLFW_KEY_Z) return glfwKey;
- // Numbers: GLFW_KEY_0 (48) -> 48
- if (glfwKey >= GLFW_KEY_0 && glfwKey <= GLFW_KEY_9) return glfwKey;
- // Special keys need mapping
- switch (glfwKey) {
- case GLFW_KEY_ESCAPE: return VK_ESCAPE;
- case GLFW_KEY_ENTER: return VK_ENTER;
- case GLFW_KEY_TAB: return VK_TAB;
- case GLFW_KEY_BACKSPACE: return VK_BACKSPACE;
- case GLFW_KEY_SPACE: return VK_SPACE;
- case GLFW_KEY_LEFT_SHIFT:
- case GLFW_KEY_RIGHT_SHIFT: return VK_SHIFT;
- case GLFW_KEY_LEFT_CONTROL:
- case GLFW_KEY_RIGHT_CONTROL: return VK_CONTROL;
- case GLFW_KEY_LEFT_ALT:
- case GLFW_KEY_RIGHT_ALT: return VK_ALT;
- case GLFW_KEY_UP: return VK_UP;
- case GLFW_KEY_DOWN: return VK_DOWN;
- case GLFW_KEY_LEFT: return VK_LEFT;
- case GLFW_KEY_RIGHT: return VK_RIGHT;
- case GLFW_KEY_F1: return VK_F1;
- case GLFW_KEY_F2: return VK_F2;
- case GLFW_KEY_F3: return VK_F3;
- case GLFW_KEY_F4: return VK_F4;
- case GLFW_KEY_F5: return VK_F5;
- case GLFW_KEY_F6: return VK_F6;
- case GLFW_KEY_F7: return VK_F7;
- case GLFW_KEY_F8: return VK_F8;
- case GLFW_KEY_F9: return VK_F9;
- case GLFW_KEY_F10: return VK_F10;
- case GLFW_KEY_F11: return VK_F11;
- case GLFW_KEY_F12: return VK_F12;
- case GLFW_KEY_INSERT: return VK_INSERT;
- case GLFW_KEY_DELETE: return VK_DELETE;
- case GLFW_KEY_HOME: return VK_HOME;
- case GLFW_KEY_END: return VK_END;
- case GLFW_KEY_PAGE_UP: return VK_PAGEUP;
- case GLFW_KEY_PAGE_DOWN: return VK_PAGEDOWN;
- default: return -1; // Unknown
- }
-}
-
-static InputRecording* globalInputRecording = nullptr;
-
-#if defined(__has_feature)
- #if __has_feature(address_sanitizer)
- #define BUTTERSCOTCH_HAS_ASAN 1
- #endif
-#endif
-#if defined(__SANITIZE_ADDRESS__)
- #define BUTTERSCOTCH_HAS_ASAN 1
-#endif
-
-#if BUTTERSCOTCH_HAS_ASAN
-void __asan_set_death_callback(void (*callback)(void));
-#endif
-
-static volatile sig_atomic_t crashSaveInProgress = 0;
-
-static void saveRecordingOnCrash(void) {
- if (crashSaveInProgress) return;
- crashSaveInProgress = 1;
- if (globalInputRecording != nullptr && globalInputRecording->isRecording) {
- InputRecording_save(globalInputRecording);
- }
-}
-
-static void crashSignalHandler(int sig) {
- saveRecordingOnCrash();
- signal(sig, SIG_DFL);
- raise(sig);
-}
-
-static void installCrashHandlers(void) {
-#if BUTTERSCOTCH_HAS_ASAN
- __asan_set_death_callback(saveRecordingOnCrash);
-#endif
- signal(SIGSEGV, crashSignalHandler);
- signal(SIGABRT, crashSignalHandler);
-#ifdef SIGBUS
- signal(SIGBUS, crashSignalHandler);
-#endif
- signal(SIGFPE, crashSignalHandler);
- signal(SIGILL, crashSignalHandler);
-}
-
-static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
- (void) scancode; (void) mods;
- Runner* runner = (Runner*) glfwGetWindowUserPointer(window);
- // During playback, suppress real keyboard input (window events like close still work)
- if (InputRecording_isPlaybackActive(globalInputRecording)) return;
- int32_t gmlKey = glfwKeyToGml(key);
- if (0 > gmlKey) return;
- if (action == GLFW_PRESS) RunnerKeyboard_onKeyDown(runner->keyboard, gmlKey);
- else if (action == GLFW_RELEASE) RunnerKeyboard_onKeyUp(runner->keyboard, gmlKey);
- // GLFW_REPEAT is ignored (GML doesn't use key repeat)
-}
-
-static void characterCallback(GLFWwindow* window, unsigned int codepoint) {
- Runner* runner = (Runner*) glfwGetWindowUserPointer(window);
- if (InputRecording_isPlaybackActive(globalInputRecording)) return;
- RunnerKeyboard_onCharacter(runner->keyboard, codepoint);
-}
-
-static void setGlfwWindowTitle(void* window, const char* title) {
- glfwSetWindowTitle((GLFWwindow*) window, title);
-}
-
-static bool getGlfwWindowFocus(void* window) {
- return glfwGetWindowAttrib((GLFWwindow*) window, GLFW_FOCUSED) != 0;
-}
-
-void saveInputRecording() {
- // Save input recording if active, then free
- if (globalInputRecording != nullptr) {
- if (globalInputRecording->isRecording) {
- InputRecording_save(globalInputRecording);
- }
- InputRecording_free(globalInputRecording);
- globalInputRecording = nullptr;
- }
-}
-
-#ifndef _WIN32
-typedef struct { int key; struct sigaction value; } PreviousSignalActionEntry;
-static PreviousSignalActionEntry* previousSignalActions = nullptr;
-
-static void onCrashSignal(int sig) {
- saveInputRecording();
- // Restore the previous handler (ASAN) and re-raise so it can report the fault
- sigaction(sig, &previousSignalActions[hmgeti(previousSignalActions, sig)].value, nullptr);
- raise(sig);
-}
-#endif
-
-// ===[ MAIN ]===
-int main(int argc, char* argv[]) {
- CommandLineArgs args;
- parseCommandLineArgs(&args, argc, argv);
-
- printf("Loading %s...\n", args.dataWinPath);
-
- DataWin* dataWin = DataWin_parse(
- args.dataWinPath,
- (DataWinParserOptions) {
- .parseGen8 = true,
- .parseOptn = true,
- .parseLang = true,
- .parseExtn = false,
- .parseSond = true,
- .parseAgrp = true,
- .parseSprt = true,
- .parseBgnd = true,
- .parsePath = true,
- .parseScpt = true,
- .parseGlob = true,
- .parseShdr = true,
- .parseFont = true,
- .parseTmln = true,
- .parseObjt = true,
- .parseRoom = true,
- .parseTpag = true,
- .parseCode = true,
- .parseVari = true,
- .parseFunc = true,
- .parseStrg = true,
- .parseTxtr = true,
- .parseAudo = true,
- .skipLoadingPreciseMasksForNonPreciseSprites = true,
- .lazyLoadRooms = args.lazyRooms,
- .eagerlyLoadedRooms = args.eagerRooms
- }
- );
-
- Gen8* gen8 = &dataWin->gen8;
- printf("Loaded \"%s\" (%d) successfully! [Bytecode Version %u / GameMaker version %u.%u.%u.%u]\n", gen8->name, gen8->gameID, gen8->bytecodeVersion, dataWin->detectedFormat.major, dataWin->detectedFormat.minor, dataWin->detectedFormat.release, dataWin->detectedFormat.build);
-
- #ifdef __GLIBC__
- {
- struct mallinfo2 mi = mallinfo2();
- printf("Memory after data.win parsing: used=%zu bytes (%.1f KB)\n", mi.uordblks, mi.uordblks / 1024.0f);
- }
- #endif
-
- // Build window title
- char windowTitle[256];
- snprintf(windowTitle, sizeof(windowTitle), "Butterscotch - %s", gen8->displayName);
-
- // Initialize VM
- VMContext* vm = VM_create(dataWin);
-
- Profiler_setEnabled(&vm->profiler, args.profilerFramesBetween > 0);
-#ifdef ENABLE_VM_OPCODE_PROFILER
- vm->opcodeProfilerEnabled = args.opcodeProfiler;
- if (vm->opcodeProfilerEnabled) {
- vm->opcodeVariantCounts = safeCalloc(256 * 256, sizeof(uint64_t));
- vm->opcodeRValueTypeCounts = safeCalloc(256 * 256, sizeof(uint64_t));
- }
-#endif
-
- if (args.hasSeed) {
- srand((unsigned int) args.seed);
- vm->hasFixedSeed = true;
- printf("Using fixed RNG seed: %d\n", args.seed);
- }
-
- if (args.printRooms) {
- // Under --lazy-rooms we load each room for display and then free it again so the dump
- // reflects what each room contains without keeping all of them resident simultaneously.
- forEachIndexed(Room, room, idx, dataWin->room.rooms, dataWin->room.count) {
- bool loadedHere = false;
- if (!room->payloadLoaded) {
- DataWin_loadRoomPayload(dataWin, (int32_t) idx);
- loadedHere = true;
- }
-
- printf("[%d] %s ()\n", idx, room->name);
-
- forEachIndexed(RoomGameObject, roomGameObject, idx2, room->gameObjects, room->gameObjectCount) {
- GameObject* gameObject = &dataWin->objt.objects[roomGameObject->objectDefinition];
- printf(
- " [%d] %s (x=%d,y=%d,persistent=%d,solid=%d,spriteId=%d,preCreateCode=%d,creationCode=%d)\n",
- idx2,
- gameObject->name,
- roomGameObject->x,
- roomGameObject->y,
- gameObject->persistent,
- gameObject->solid,
- gameObject->spriteId,
- roomGameObject->preCreateCode,
- roomGameObject->creationCode
- );
- }
-
- if (loadedHere && !room->eagerlyLoaded) {
- DataWin_freeRoomPayload(room);
- }
- }
- VM_free(vm);
- DataWin_free(dataWin);
- return 0;
- }
-
- if (args.printDeclaredFunctions) {
- repeat(hmlen(vm->codeIndexByName), i) {
- printf("[%d] %s\n", vm->codeIndexByName[i].value, vm->codeIndexByName[i].key);
- }
- VM_free(vm);
- DataWin_free(dataWin);
- return 0;
- }
-
- if (shlen(args.disassemble) > 0) {
- VM_buildCrossReferences(vm);
- if (shgeti(args.disassemble, "*") >= 0) {
- repeat(dataWin->code.count, i) {
- VM_disassemble(vm, (int32_t) i);
- }
- } else {
- for (ptrdiff_t i = 0; shlen(args.disassemble) > i; i++) {
- const char* name = args.disassemble[i].key;
- ptrdiff_t idx = shgeti(vm->codeIndexByName, (char*) name);
- if (idx >= 0) {
- VM_disassemble(vm, vm->codeIndexByName[idx].value);
- } else {
- fprintf(stderr, "Error: Script '%s' not found in funcMap\n", name);
- }
- }
- }
- VM_free(vm);
- DataWin_free(dataWin);
- freeCommandLineArgs(&args);
- return 0;
- }
-
- // Initialize the file system
- GlfwFileSystem* glfwFileSystem = GlfwFileSystem_create(args.dataWinPath);
-
- // Init GLFW
- glfwSetErrorCallback(glfwErrorCallback);
- if (!glfwInit()) {
- fprintf(stderr, "Failed to initialize GLFW\n");
- DataWin_free(dataWin);
- freeCommandLineArgs(&args);
- return 1;
- }
-
- bool modernGL = strcmp(args.renderer, "legacy-gl") != 0;
- if (!modernGL) {
- glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 1);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
- } else {
-#ifdef ENABLE_GLES
- glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
-#else
- glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
- glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
- glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
- glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
-#endif
- }
-
- // Load SDL gamecontroller mappings
- {
- const char* dbPath = "gamecontrollerdb.txt";
- FILE* f = fopen(dbPath, "r");
- if (f != NULL) {
- fseek(f, 0, SEEK_END);
- long len = ftell(f);
- fseek(f, 0, SEEK_SET);
- char* buffer = (char*) malloc(len + 1);
- if (buffer != NULL) {
- fread(buffer, 1, len, f);
- buffer[len] = '\0';
- GlfwGamepad_loadMappings(buffer);
- free(buffer);
- }
- fclose(f);
- } else {
- fprintf(stderr, "Gamepad: SDL gamecontrollerdb.txt not found at %s, using defaults\n", dbPath);
- }
- }
-
- if (args.headless) {
- glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
- }
-
- GLFWwindow* window = glfwCreateWindow((int) gen8->defaultWindowWidth, (int) gen8->defaultWindowHeight, windowTitle, nullptr, nullptr);
- if (window == nullptr) {
- fprintf(stderr, "Failed to create GLFW window\n");
- glfwTerminate();
- DataWin_free(dataWin);
- freeCommandLineArgs(&args);
- return 1;
- }
-
- glfwMakeContextCurrent(window);
- glfwSwapInterval(0); // Disable v-sync, we control timing ourselves
-
- // Load OpenGL function pointers via GLAD
-#ifdef ENABLE_GLES
- if (!gladLoadGLES2Loader((GLADloadproc) glfwGetProcAddress)) {
-#else
- if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
-#endif
- fprintf(stderr, "Failed to initialize GLAD\n");
- glfwDestroyWindow(window);
- glfwTerminate();
- DataWin_free(dataWin);
- freeCommandLineArgs(&args);
- return 1;
- }
-
- // Install the OpenGL debug message callback
-#ifndef ENABLE_GLES
- if (modernGL)
- installGLDebugCallback();
-#endif
-
- // Initialize the renderer
- Renderer* renderer = nullptr;
-#ifdef ENABLE_GLES
- if (strcmp(args.renderer, "legacy-gl") == 0) {
- fprintf(stderr, "--renderer legacy-gl is not available in GLES builds; falling back to gl\n");
- }
- renderer = GLRenderer_create();
-#else
- if(strcmp(args.renderer, "legacy-gl") == 0)
- renderer = GLLegacyRenderer_create();
- else
- renderer = GLRenderer_create();
-#endif
-
- // Initialize the audio system
- AudioSystem* audioSystem = nullptr;
- if (!args.headless) {
- audioSystem = (AudioSystem*) MaAudioSystem_create();
- } else {
- audioSystem = (AudioSystem*) NoopAudioSystem_create();
- }
-
- // Initialize the runner
- Runner* runner = Runner_create(dataWin, vm, renderer, (FileSystem*) glfwFileSystem, audioSystem);
- runner->debugMode = args.debug;
- runner->osType = args.osType;
- runner->nativeWindow = window;
- runner->setWindowTitle = setGlfwWindowTitle;
- runner->windowHasFocus = getGlfwWindowFocus;
-
- // Set up input recording/playback (both can be active: playback then continue recording)
- if (args.playbackInputsPath != nullptr) {
- globalInputRecording = InputRecording_createPlayer(args.playbackInputsPath, args.recordInputsPath);
- } else if (args.recordInputsPath != nullptr) {
- globalInputRecording = InputRecording_createRecorder(args.recordInputsPath);
- }
- if (globalInputRecording != nullptr) {
- installCrashHandlers();
- }
- shcopyFromTo(args.varReadsToBeTraced, runner->vmContext->varReadsToBeTraced);
- shcopyFromTo(args.varWritesToBeTraced, runner->vmContext->varWritesToBeTraced);
- shcopyFromTo(args.functionCallsToBeTraced, runner->vmContext->functionCallsToBeTraced);
- shcopyFromTo(args.alarmsToBeTraced, runner->vmContext->alarmsToBeTraced);
- shcopyFromTo(args.instanceLifecyclesToBeTraced, runner->vmContext->instanceLifecyclesToBeTraced);
- shcopyFromTo(args.eventsToBeTraced, runner->vmContext->eventsToBeTraced);
- shcopyFromTo(args.opcodesToBeTraced, runner->vmContext->opcodesToBeTraced);
- shcopyFromTo(args.stackToBeTraced, runner->vmContext->stackToBeTraced);
- shcopyFromTo(args.tilesToBeTraced, runner->vmContext->tilesToBeTraced);
- runner->vmContext->traceBytecodeAfterFrame = args.traceBytecodeAfterFrame;
- runner->vmContext->alwaysLogUnknownFunctions = args.alwaysLogUnknownFunctions;
- runner->vmContext->alwaysLogStubbedFunctions = args.alwaysLogStubbedFunctions;
- runner->vmContext->traceEventInherited = args.traceEventInherited;
-
- // Set up keyboard input
- glfwSetWindowUserPointer(window, runner);
- glfwSetKeyCallback(window, keyCallback);
- glfwSetCharCallback(window, characterCallback);
-
-#ifndef _WIN32
- struct sigaction sa = { .sa_handler = onCrashSignal };
- sigemptyset(&sa.sa_mask);
- struct sigaction prev;
- sigaction(SIGABRT, &sa, &prev);
- hmput(previousSignalActions, SIGABRT, prev);
- sigaction(SIGSEGV, &sa, &prev);
- hmput(previousSignalActions, SIGSEGV, prev);
-#endif
-
- // Initialize the first room and fire Game Start / Room Start events
- Runner_initFirstRoom(runner);
-
- // Main loop
- bool debugPaused = false;
- bool debugShowCollisionMasks = false;
- double lastFrameTime = glfwGetTime();
- while (!glfwWindowShouldClose(window) && !runner->shouldExit) {
- // Clear last frame's pressed/released state, then poll new input events
- RunnerKeyboard_beginFrame(runner->keyboard);
- RunnerGamepad_beginFrame(runner->gamepads);
- glfwPollEvents();
- GlfwGamepad_poll(runner->gamepads);
-
- // Process input recording/playback (must happen after glfwPollEvents, before Runner_step)
- InputRecording_processFrame(globalInputRecording, runner->keyboard, runner->frameCount);
-
- // Debug key bindings
- if (runner->debugMode) {
- // Pause
- if (RunnerKeyboard_checkPressed(runner->keyboard, 'P')) {
- debugPaused = !debugPaused;
- fprintf(stderr, "Debug: %s\n", debugPaused ? "Paused" : "Resumed");
- }
-
- // Go to next room
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEUP)) {
- DataWin* dw = runner->dataWin;
- if ((int32_t) dw->gen8.roomOrderCount > runner->currentRoomOrderPosition + 1) {
- int32_t nextIdx = dw->gen8.roomOrder[runner->currentRoomOrderPosition + 1];
- runner->pendingRoom = nextIdx;
- runner->audioSystem->vtable->stopAll(runner->audioSystem);
- fprintf(stderr, "Debug: Going to next room -> %s\n", dw->room.rooms[nextIdx].name);
- }
- }
-
- // Go to previous room
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEDOWN)) {
- DataWin* dw = runner->dataWin;
- if (runner->currentRoomOrderPosition > 0) {
- int32_t prevIdx = dw->gen8.roomOrder[runner->currentRoomOrderPosition - 1];
- runner->pendingRoom = prevIdx;
- runner->audioSystem->vtable->stopAll(runner->audioSystem);
- fprintf(stderr, "Debug: Going to previous room -> %s\n", dw->room.rooms[prevIdx].name);
- }
- }
-
- // Dump runner state to console
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F12)) {
- fprintf(stderr, "Debug: Dumping runner state at frame %d\n", runner->frameCount);
- Runner_dumpState(runner);
- }
-
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F11)) {
- fprintf(stderr, "Debug: Dumping runner state at frame %d\n", runner->frameCount);
- char* json = Runner_dumpStateJson(runner);
-
- if (args.dumpJsonFilePattern != nullptr) {
- char filename[512];
- snprintf(filename, sizeof(filename), args.dumpJsonFilePattern, runner->frameCount);
- FILE* f = fopen(filename, "w");
- if (f != nullptr) {
- fwrite(json, 1, strlen(json), f);
- fputc('\n', f);
- fclose(f);
- printf("JSON dump saved: %s\n", filename);
- } else {
- fprintf(stderr, "Error: Could not write JSON dump to '%s'\n", filename);
- }
- } else {
- printf("%s\n", json);
- }
-
- free(json);
- }
-
- // Toggle the collision mask debug overlay
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F2)) {
- debugShowCollisionMasks = !debugShowCollisionMasks;
- fprintf(stderr, "Debug: Collision mask overlay %s!\n", debugShowCollisionMasks ? "enabled" : "disabled");
- }
-
- // Reset global interact state because I HATE when I get stuck while moving through rooms
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F10)) {
- int32_t interactVarId = shget(runner->vmContext->globalVarNameMap, "interact");
-
- runner->vmContext->globalVars[interactVarId] = RValue_makeInt32(0);
- printf("Changed global.interact [%d] value!\n", interactVarId);
- }
- }
-
- // Run the game step if the game is paused
- bool shouldStep = true;
- if (runner->debugMode && debugPaused) {
- shouldStep = RunnerKeyboard_checkPressed(runner->keyboard, 'O');
- if (shouldStep) fprintf(stderr, "Debug: Frame advance (frame %d)\n", runner->frameCount);
- }
-
- double frameStartTime = 0;
-
- if (shouldStep) {
- if (args.traceFrames) {
- frameStartTime = glfwGetTime();
- fprintf(stderr, "Frame %d (Start)\n", runner->frameCount);
- }
-
- // Run one game step (Begin Step, Keyboard, Alarms, Step, End Step, room transitions)
- Runner_step(runner);
-
- if (args.profilerFramesBetween > 0 && runner->frameCount > 0 && runner->frameCount % args.profilerFramesBetween == 0) {
- char* profilerReport = Profiler_createReport(vm->profiler, 20, args.profilerFramesBetween);
- if (profilerReport != nullptr) {
- fprintf(stderr, "%s\n", profilerReport);
- free(profilerReport);
- }
- Profiler_reset(vm->profiler);
- }
-
- // Update audio system (gain fading, cleanup ended sounds)
- float dt = (float) (glfwGetTime() - lastFrameTime);
- if (0.0f > dt) dt = 0.0f;
- if (dt > 0.1f) dt = 0.1f; // cap delta to avoid huge fades on lag spikes
- runner->audioSystem->vtable->update(runner->audioSystem, dt);
-
- // Dump full runner state if this frame was requested
- if (hmget(args.dumpFrames, runner->frameCount)) {
- Runner_dumpState(runner);
- }
-
- // Dump runner state as JSON if this frame was requested
- if (hmget(args.dumpJsonFrames, runner->frameCount)) {
- char* json = Runner_dumpStateJson(runner);
- if (args.dumpJsonFilePattern != nullptr) {
- char filename[512];
- snprintf(filename, sizeof(filename), args.dumpJsonFilePattern, runner->frameCount);
- FILE* f = fopen(filename, "w");
- if (f != nullptr) {
- fwrite(json, 1, strlen(json), f);
- fputc('\n', f);
- fclose(f);
- printf("JSON dump saved: %s\n", filename);
- } else {
- fprintf(stderr, "Error: Could not write JSON dump to '%s'\n", filename);
- }
- } else {
- printf("%s\n", json);
- }
- free(json);
- }
- }
-
- // Query actual framebuffer size (differs from window size on Wayland with fractional scaling)
- int fbWidth, fbHeight;
- glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
-
- // Clear the default framebuffer (window background) to black
- if (!(strcmp(args.renderer, "legacy-gl") == 0)) {
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- }
- glClear(GL_COLOR_BUFFER_BIT);
-
- int32_t gameW = (int32_t) gen8->defaultWindowWidth;
- int32_t gameH = (int32_t) gen8->defaultWindowHeight;
-
- // The application surface (FBO) is sized to defaultWindowWidth x defaultWindowHeight.
- // It is a bit hard to understand, but here's how it works:
- // The Port X/Port Y controls the position of the game viewport within the application surface.
- // The Port W/Port H controls the size of the game viewport within the application surface.
- // Think of it like if you had an image (or... well, a framebuffer) and you are "pasting" it over the application surface.
- // And the Port W/Port H are scaled by the window size too (set by the GEN8 chunk)
- float displayScaleX;
- float displayScaleY;
-
- Runner_computeViewDisplayScale(runner, gameW, gameH, &displayScaleX, &displayScaleY);
-
- renderer->vtable->beginFrame(renderer, gameW, gameH, fbWidth, fbHeight);
-
- // Clear FBO with room background color
- if (runner->drawBackgroundColor) {
- int rInt = BGR_R(runner->backgroundColor);
- int gInt = BGR_G(runner->backgroundColor);
- int bInt = BGR_B(runner->backgroundColor);
- glClearColor(rInt / 255.0f, gInt / 255.0f, bInt / 255.0f, 1.0f);
- } else {
- glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
- }
- glClear(GL_COLOR_BUFFER_BIT);
-
- Runner_drawViews(runner, gameW, gameH, displayScaleX, displayScaleY, debugShowCollisionMasks);
-
- renderer->vtable->endFrame(renderer);
-
- // Capture screenshot if this frame matches a requested frame
- bool shouldScreenshot = hmget(args.screenshotFrames, runner->frameCount);
-
- if (shouldScreenshot) {
- // Bind FBO so glReadPixels reads from the game's native-resolution texture
- GLRenderer* gl = (GLRenderer*) renderer;
- if (!(strcmp(args.renderer, "legacy-gl") == 0))
- glBindFramebuffer(GL_READ_FRAMEBUFFER, gl->fbo);
- captureScreenshot(args.screenshotPattern, runner->frameCount, gameW, gameH);
- if (!(strcmp(args.renderer, "legacy-gl") == 0))
- glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
- }
-
- if (args.exitAtFrame >= 0 && runner->frameCount >= args.exitAtFrame) {
- printf("Exiting at frame %d (--exit-at-frame)\n", runner->frameCount);
- glfwSetWindowShouldClose(window, GLFW_TRUE);
- }
-
- if (shouldStep && args.traceFrames) {
- double frameElapsedMs = (glfwGetTime() - frameStartTime) * 1000.0;
- fprintf(stderr, "Frame %d (End, %.2f ms)\n", runner->frameCount, frameElapsedMs);
- }
-
- glfwSwapBuffers(window);
-
- // Limit frame rate to room speed (skip in headless mode for max speed!!)
- if (!args.headless && runner->currentRoom->speed > 0) {
- static bool fastForwardActive = false;
- static bool fastForwardTabPrev = false;
- bool fastForwardTabNow = glfwGetKey(window, GLFW_KEY_TAB) == GLFW_PRESS;
- if (args.fastForwardSpeed > 0.0 && fastForwardTabNow && !fastForwardTabPrev) {
- fastForwardActive = !fastForwardActive;
- lastFrameTime = glfwGetTime();
- }
- fastForwardTabPrev = fastForwardTabNow;
- double effectiveSpeed = (args.fastForwardSpeed > 0.0 && fastForwardActive) ? args.fastForwardSpeed : args.speedMultiplier;
- double targetFrameTime = 1.0 / (runner->currentRoom->speed * effectiveSpeed);
- double nextFrameTime = lastFrameTime + targetFrameTime;
- // Sleep for most of the remaining time, then spin-wait for precision
- double remaining = nextFrameTime - glfwGetTime();
- if (remaining > 0.002) {
- #ifdef _WIN32
- Sleep((DWORD) ((remaining - 0.001) * 1000));
- #else
- struct timespec ts = {
- .tv_sec = 0,
- .tv_nsec = (long) ((remaining - 0.001) * 1e9)
- };
- nanosleep(&ts, nullptr);
- #endif
- }
- while (glfwGetTime() < nextFrameTime) {
- // Spin-wait for the remaining sub-millisecond
- }
- lastFrameTime = nextFrameTime;
- } else {
- lastFrameTime = glfwGetTime();
- }
- }
-
- saveInputRecording();
-
- // Cleanup
- runner->audioSystem->vtable->destroy(runner->audioSystem);
- runner->audioSystem = nullptr;
- renderer->vtable->destroy(renderer);
-
- glfwDestroyWindow(window);
- glfwTerminate();
-
- Runner_free(runner);
- GlfwFileSystem_destroy(glfwFileSystem);
-#ifdef ENABLE_VM_OPCODE_PROFILER
- VM_printOpcodeProfilerReport(vm);
-#endif
- VM_free(vm);
- DataWin_free(dataWin);
-
- freeCommandLineArgs(&args);
-
- printf("Bye! :3\n");
- return 0;
-}
diff --git a/src/glfw/stb_impl.c b/src/glfw/stb_impl.c
deleted file mode 100644
index 9edfcf40..00000000
--- a/src/glfw/stb_impl.c
+++ /dev/null
@@ -1,8 +0,0 @@
-#define STB_IMAGE_IMPLEMENTATION
-#include "stb_image.h"
-
-#define STB_IMAGE_WRITE_IMPLEMENTATION
-#include "stb_image_write.h"
-
-#define STB_DS_IMPLEMENTATION
-#include "stb_ds.h"
diff --git a/src/ini.c b/src/ini.c
index b8137079..180ee524 100644
--- a/src/ini.c
+++ b/src/ini.c
@@ -1,326 +1,326 @@
-#include "ini.h"
-#include "utils.h"
-
-#include
-#include
-#include
-
-#include "text_utils.h"
-
-// ===[ Internal Helpers ]===
-
-static IniSection* findSection(const IniFile* ini, const char* name) {
- repeat(ini->count, i) {
- if (strcmp(ini->sections[i].name, name) == 0) {
- return &ini->sections[i];
- }
- }
- return nullptr;
-}
-
-static int findKeyIndex(const IniSection* section, const char* key) {
- repeat(section->count, i) {
- if (strcmp(section->keys[i], key) == 0) {
- return i;
- }
- }
- return -1;
-}
-
-static IniSection* addSection(IniFile* ini, const char* name) {
- if (ini->count >= ini->capacity) {
- ini->capacity = (ini->capacity == 0) ? 4 : ini->capacity * 2;
- ini->sections = safeRealloc(ini->sections, (size_t) ini->capacity * sizeof(IniSection));
- }
- IniSection* section = &ini->sections[ini->count++];
- section->name = safeStrdup(name);
- section->keys = nullptr;
- section->values = nullptr;
- section->count = 0;
- section->capacity = 0;
- return section;
-}
-
-static void addKeyValue(IniSection* section, const char* key, const char* value) {
- if (section->count >= section->capacity) {
- section->capacity = (section->capacity == 0) ? 4 : section->capacity * 2;
- section->keys = safeRealloc(section->keys, (size_t) section->capacity * sizeof(char*));
- section->values = safeRealloc(section->values, (size_t) section->capacity * sizeof(char*));
- }
- section->keys[section->count] = safeStrdup(key);
- section->values[section->count] = safeStrdup(value);
- section->count++;
-}
-
-static const char* skipWhitespace(const char* p) {
- while (TextUtils_isWhitespaceChar(*p)) {
- p++;
- }
- return p;
-}
-
-static char* normalizeValue(char* value) {
- TextUtils_trimTrailingWhitespace(value);
- size_t length = strlen(value);
- if (length >= 2 && value[0] == '"' && value[length - 1] == '"') {
- value[length - 1] = '\0';
- return safeStrdup(value + 1);
- }
- return safeStrdup(value);
-}
-
-// ===[ Lifecycle ]===
-
-IniFile* Ini_parse(const char* text) {
- IniFile* ini = safeCalloc(1, sizeof(IniFile));
-
- if (text == nullptr || *text == '\0') {
- return ini;
- }
-
- // Make a mutable copy to tokenize
- char* data = safeStrdup(text);
- IniSection* currentSection = nullptr;
-
- char* line = data;
- while (line != nullptr) {
- // Find end of line
- char* eol = strchr(line, '\n');
- if (eol != nullptr) {
- *eol = '\0';
- }
-
- // Trim leading whitespace
- const char* trimmed = skipWhitespace(line);
-
- // Skip empty lines and comments
- if (*trimmed == '\0' || *trimmed == ';' || *trimmed == '#') {
- line = eol != nullptr ? eol + 1 : nullptr;
- continue;
- }
-
- // Strip trailing whitespace/CR
- char* mutableTrimmed = (char*) trimmed;
- TextUtils_trimTrailingWhitespace(mutableTrimmed);
-
- if (*trimmed == '[') {
- // Section header
- const char* nameStart = trimmed + 1;
- char* closeBracket = strchr(mutableTrimmed, ']');
- if (closeBracket != nullptr) {
- *closeBracket = '\0';
- currentSection = findSection(ini, nameStart);
- if (currentSection == nullptr) {
- currentSection = addSection(ini, nameStart);
- }
- } else {
- fprintf(stderr, "Ini: malformed section header: %s\n", trimmed);
- }
- } else {
- // Key=value pair
- char* equals = strchr(mutableTrimmed, '=');
- if (equals != nullptr && currentSection != nullptr) {
- *equals = '\0';
- char* key = mutableTrimmed;
- char* value = equals + 1;
- TextUtils_trimTrailingWhitespace(key);
- value = (char*) skipWhitespace(value);
- char* normalizedValue = normalizeValue(value);
-
- // Check if key already exists - overwrite if so
- int existingIndex = findKeyIndex(currentSection, key);
- if (existingIndex >= 0) {
- free(currentSection->values[existingIndex]);
- currentSection->values[existingIndex] = normalizedValue;
- } else {
- addKeyValue(currentSection, key, normalizedValue);
- free(normalizedValue);
- }
- }
- // Silently skip key=value lines outside any section (matching GML behavior)
- }
-
- line = eol != nullptr ? eol + 1 : nullptr;
- }
-
- free(data);
- return ini;
-}
-
-void Ini_free(IniFile* ini) {
- if (ini == nullptr)
- return;
-
- repeat(ini->count, i) {
- IniSection* section = &ini->sections[i];
- repeat(section->count, j) {
- free(section->keys[j]);
- free(section->values[j]);
- }
- free(section->keys);
- free(section->values);
- free(section->name);
- }
- free(ini->sections);
- free(ini);
-}
-
-// ===[ Queries ]===
-
-const char* Ini_getString(const IniFile* ini, const char* section, const char* key) {
- IniSection* sec = findSection(ini, section);
- if (sec == nullptr)
- return nullptr;
-
- int idx = findKeyIndex(sec, key);
- if (0 > idx)
- return nullptr;
-
- return sec->values[idx];
-}
-
-bool Ini_hasSection(const IniFile* ini, const char* section) {
- return findSection(ini, section) != nullptr;
-}
-
-bool Ini_hasKey(const IniFile* ini, const char* section, const char* key) {
- IniSection* sec = findSection(ini, section);
-
- if (sec == nullptr)
- return false;
-
- return findKeyIndex(sec, key) >= 0;
-}
-
-// ===[ Mutation ]===
-
-void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value) {
- // If we are passing a null value, let's remove it!
- if (value == nullptr) {
- Ini_deleteKey(ini, section, key);
- return;
- }
-
- IniSection* sec = findSection(ini, section);
- if (sec == nullptr) {
- sec = addSection(ini, section);
- }
-
- int idx = findKeyIndex(sec, key);
- if (idx >= 0) {
- free(sec->values[idx]);
- sec->values[idx] = safeStrdup(value);
- } else {
- addKeyValue(sec, key, value);
- }
-}
-
-void Ini_deleteKey(IniFile* ini, const char* section, const char* key) {
- IniSection* sec = findSection(ini, section);
- if (sec == nullptr)
- return;
-
- int idx = findKeyIndex(sec, key);
- if (0 > idx)
- return;
-
- free(sec->keys[idx]);
- free(sec->values[idx]);
-
- // Shift remaining entries down
- for (int i = idx; sec->count - 1 > i; i++) {
- sec->keys[i] = sec->keys[i + 1];
- sec->values[i] = sec->values[i + 1];
- }
- sec->count--;
-}
-
-void Ini_deleteSection(IniFile* ini, const char* section) {
- int sectionIndex = -1;
- repeat(ini->count, i) {
- if (strcmp(ini->sections[i].name, section) == 0) {
- sectionIndex = (int) i;
- break;
- }
- }
- if (0 > sectionIndex) return;
-
- // Free the section's contents
- IniSection* sec = &ini->sections[sectionIndex];
- repeat(sec->count, j) {
- free(sec->keys[j]);
- free(sec->values[j]);
- }
- free(sec->keys);
- free(sec->values);
- free(sec->name);
-
- // Shift remaining sections down
- for (int i = sectionIndex; ini->count - 1 > i; i++) {
- ini->sections[i] = ini->sections[i + 1];
- }
- ini->count--;
-}
-
-// ===[ Serialization ]===
-
-char* Ini_serialize(const IniFile* ini, size_t initialCapacity) {
- size_t capacity = initialCapacity;
- size_t length = 0;
- char* buffer = safeMalloc(capacity);
- buffer[0] = '\0';
-
- repeat(ini->count, i) {
- IniSection* section = &ini->sections[i];
-
- // Blank line before section (unless at start of output)
- // Format: \n[name]\nkey=value\n...
- const char* name = section->name;
- size_t nameLen = strlen(name);
-
- // Calculate space needed for section header
- // optional \n + [ + name + ] + \n
- size_t needed = (length > 0 ? 1 : 0) + 1 + nameLen + 1 + 1;
-
- // Calculate space needed for all key=value pairs
- repeat(section->count, j) {
- needed += strlen(section->keys[j]) + 2 + strlen(section->values[j]) + 2;
- }
-
- // Grow buffer if needed
- while (length + needed + 1 > capacity) {
- capacity *= 2;
- }
- buffer = safeRealloc(buffer, capacity);
-
- // Write section header
- if (length > 0) {
- buffer[length++] = '\n';
- }
- buffer[length++] = '[';
- memcpy(buffer + length, name, nameLen);
- length += nameLen;
- buffer[length++] = ']';
- buffer[length++] = '\n';
-
- // Write key=value pairs
- repeat(section->count, j) {
- const char* key = section->keys[j];
- const char* value = section->values[j];
- size_t keyLen = strlen(key);
- size_t valueLen = strlen(value);
-
- memcpy(buffer + length, key, keyLen);
- length += keyLen;
- buffer[length++] = '=';
- buffer[length++] = '"';
- memcpy(buffer + length, value, valueLen);
- length += valueLen;
- buffer[length++] = '"';
- buffer[length++] = '\n';
- }
- }
-
- buffer[length] = '\0';
- return buffer;
-}
+#include "ini.h"
+#include "utils.h"
+
+#include
+#include
+#include
+
+#include "text_utils.h"
+
+// ===[ Internal Helpers ]===
+
+static IniSection* findSection(const IniFile* ini, const char* name) {
+ repeat(ini->count, i) {
+ if (strcmp(ini->sections[i].name, name) == 0) {
+ return &ini->sections[i];
+ }
+ }
+ return nullptr;
+}
+
+static int findKeyIndex(const IniSection* section, const char* key) {
+ repeat(section->count, i) {
+ if (strcmp(section->keys[i], key) == 0) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+static IniSection* addSection(IniFile* ini, const char* name) {
+ if (ini->count >= ini->capacity) {
+ ini->capacity = (ini->capacity == 0) ? 4 : ini->capacity * 2;
+ ini->sections = safeRealloc(ini->sections, (size_t) ini->capacity * sizeof(IniSection));
+ }
+ IniSection* section = &ini->sections[ini->count++];
+ section->name = safeStrdup(name);
+ section->keys = nullptr;
+ section->values = nullptr;
+ section->count = 0;
+ section->capacity = 0;
+ return section;
+}
+
+static void addKeyValue(IniSection* section, const char* key, const char* value) {
+ if (section->count >= section->capacity) {
+ section->capacity = (section->capacity == 0) ? 4 : section->capacity * 2;
+ section->keys = safeRealloc(section->keys, (size_t) section->capacity * sizeof(char*));
+ section->values = safeRealloc(section->values, (size_t) section->capacity * sizeof(char*));
+ }
+ section->keys[section->count] = safeStrdup(key);
+ section->values[section->count] = safeStrdup(value);
+ section->count++;
+}
+
+static const char* skipWhitespace(const char* p) {
+ while (TextUtils_isWhitespaceChar(*p)) {
+ p++;
+ }
+ return p;
+}
+
+static char* normalizeValue(char* value) {
+ TextUtils_trimTrailingWhitespace(value);
+ size_t length = strlen(value);
+ if (length >= 2 && value[0] == '"' && value[length - 1] == '"') {
+ value[length - 1] = '\0';
+ return safeStrdup(value + 1);
+ }
+ return safeStrdup(value);
+}
+
+// ===[ Lifecycle ]===
+
+IniFile* Ini_parse(const char* text) {
+ IniFile* ini = safeCalloc(1, sizeof(IniFile));
+
+ if (text == nullptr || *text == '\0') {
+ return ini;
+ }
+
+ // Make a mutable copy to tokenize
+ char* data = safeStrdup(text);
+ IniSection* currentSection = nullptr;
+
+ char* line = data;
+ while (line != nullptr) {
+ // Find end of line
+ char* eol = strchr(line, '\n');
+ if (eol != nullptr) {
+ *eol = '\0';
+ }
+
+ // Trim leading whitespace
+ const char* trimmed = skipWhitespace(line);
+
+ // Skip empty lines and comments
+ if (*trimmed == '\0' || *trimmed == ';' || *trimmed == '#') {
+ line = eol != nullptr ? eol + 1 : nullptr;
+ continue;
+ }
+
+ // Strip trailing whitespace/CR
+ char* mutableTrimmed = (char*) trimmed;
+ TextUtils_trimTrailingWhitespace(mutableTrimmed);
+
+ if (*trimmed == '[') {
+ // Section header
+ const char* nameStart = trimmed + 1;
+ char* closeBracket = strchr(mutableTrimmed, ']');
+ if (closeBracket != nullptr) {
+ *closeBracket = '\0';
+ currentSection = findSection(ini, nameStart);
+ if (currentSection == nullptr) {
+ currentSection = addSection(ini, nameStart);
+ }
+ } else {
+ fprintf(stderr, "Ini: malformed section header: %s\n", trimmed);
+ }
+ } else {
+ // Key=value pair
+ char* equals = strchr(mutableTrimmed, '=');
+ if (equals != nullptr && currentSection != nullptr) {
+ *equals = '\0';
+ char* key = mutableTrimmed;
+ char* value = equals + 1;
+ TextUtils_trimTrailingWhitespace(key);
+ value = (char*) skipWhitespace(value);
+ char* normalizedValue = normalizeValue(value);
+
+ // Check if key already exists - overwrite if so
+ int existingIndex = findKeyIndex(currentSection, key);
+ if (existingIndex >= 0) {
+ free(currentSection->values[existingIndex]);
+ currentSection->values[existingIndex] = normalizedValue;
+ } else {
+ addKeyValue(currentSection, key, normalizedValue);
+ free(normalizedValue);
+ }
+ }
+ // Silently skip key=value lines outside any section (matching GML behavior)
+ }
+
+ line = eol != nullptr ? eol + 1 : nullptr;
+ }
+
+ free(data);
+ return ini;
+}
+
+void Ini_free(IniFile* ini) {
+ if (ini == nullptr)
+ return;
+
+ repeat(ini->count, i) {
+ IniSection* section = &ini->sections[i];
+ repeat(section->count, j) {
+ free(section->keys[j]);
+ free(section->values[j]);
+ }
+ free(section->keys);
+ free(section->values);
+ free(section->name);
+ }
+ free(ini->sections);
+ free(ini);
+}
+
+// ===[ Queries ]===
+
+const char* Ini_getString(const IniFile* ini, const char* section, const char* key) {
+ IniSection* sec = findSection(ini, section);
+ if (sec == nullptr)
+ return nullptr;
+
+ int idx = findKeyIndex(sec, key);
+ if (0 > idx)
+ return nullptr;
+
+ return sec->values[idx];
+}
+
+bool Ini_hasSection(const IniFile* ini, const char* section) {
+ return findSection(ini, section) != nullptr;
+}
+
+bool Ini_hasKey(const IniFile* ini, const char* section, const char* key) {
+ IniSection* sec = findSection(ini, section);
+
+ if (sec == nullptr)
+ return false;
+
+ return findKeyIndex(sec, key) >= 0;
+}
+
+// ===[ Mutation ]===
+
+void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value) {
+ // If we are passing a null value, let's remove it!
+ if (value == nullptr) {
+ Ini_deleteKey(ini, section, key);
+ return;
+ }
+
+ IniSection* sec = findSection(ini, section);
+ if (sec == nullptr) {
+ sec = addSection(ini, section);
+ }
+
+ int idx = findKeyIndex(sec, key);
+ if (idx >= 0) {
+ free(sec->values[idx]);
+ sec->values[idx] = safeStrdup(value);
+ } else {
+ addKeyValue(sec, key, value);
+ }
+}
+
+void Ini_deleteKey(IniFile* ini, const char* section, const char* key) {
+ IniSection* sec = findSection(ini, section);
+ if (sec == nullptr)
+ return;
+
+ int idx = findKeyIndex(sec, key);
+ if (0 > idx)
+ return;
+
+ free(sec->keys[idx]);
+ free(sec->values[idx]);
+
+ // Shift remaining entries down
+ for (int i = idx; sec->count - 1 > i; i++) {
+ sec->keys[i] = sec->keys[i + 1];
+ sec->values[i] = sec->values[i + 1];
+ }
+ sec->count--;
+}
+
+void Ini_deleteSection(IniFile* ini, const char* section) {
+ int sectionIndex = -1;
+ repeat(ini->count, i) {
+ if (strcmp(ini->sections[i].name, section) == 0) {
+ sectionIndex = (int) i;
+ break;
+ }
+ }
+ if (0 > sectionIndex) return;
+
+ // Free the section's contents
+ IniSection* sec = &ini->sections[sectionIndex];
+ repeat(sec->count, j) {
+ free(sec->keys[j]);
+ free(sec->values[j]);
+ }
+ free(sec->keys);
+ free(sec->values);
+ free(sec->name);
+
+ // Shift remaining sections down
+ for (int i = sectionIndex; ini->count - 1 > i; i++) {
+ ini->sections[i] = ini->sections[i + 1];
+ }
+ ini->count--;
+}
+
+// ===[ Serialization ]===
+
+char* Ini_serialize(const IniFile* ini, size_t initialCapacity) {
+ size_t capacity = initialCapacity;
+ size_t length = 0;
+ char* buffer = safeMalloc(capacity);
+ buffer[0] = '\0';
+
+ repeat(ini->count, i) {
+ IniSection* section = &ini->sections[i];
+
+ // Blank line before section (unless at start of output)
+ // Format: \n[name]\nkey=value\n...
+ const char* name = section->name;
+ size_t nameLen = strlen(name);
+
+ // Calculate space needed for section header
+ // optional \n + [ + name + ] + \n
+ size_t needed = (length > 0 ? 1 : 0) + 1 + nameLen + 1 + 1;
+
+ // Calculate space needed for all key=value pairs
+ repeat(section->count, j) {
+ needed += strlen(section->keys[j]) + 2 + strlen(section->values[j]) + 2;
+ }
+
+ // Grow buffer if needed
+ while (length + needed + 1 > capacity) {
+ capacity *= 2;
+ }
+ buffer = safeRealloc(buffer, capacity);
+
+ // Write section header
+ if (length > 0) {
+ buffer[length++] = '\n';
+ }
+ buffer[length++] = '[';
+ memcpy(buffer + length, name, nameLen);
+ length += nameLen;
+ buffer[length++] = ']';
+ buffer[length++] = '\n';
+
+ // Write key=value pairs
+ repeat(section->count, j) {
+ const char* key = section->keys[j];
+ const char* value = section->values[j];
+ size_t keyLen = strlen(key);
+ size_t valueLen = strlen(value);
+
+ memcpy(buffer + length, key, keyLen);
+ length += keyLen;
+ buffer[length++] = '=';
+ buffer[length++] = '"';
+ memcpy(buffer + length, value, valueLen);
+ length += valueLen;
+ buffer[length++] = '"';
+ buffer[length++] = '\n';
+ }
+ }
+
+ buffer[length] = '\0';
+ return buffer;
+}
diff --git a/src/ini.h b/src/ini.h
index 8a7dddb8..9a7188ac 100644
--- a/src/ini.h
+++ b/src/ini.h
@@ -1,43 +1,43 @@
-#pragma once
-
-#include "common.h"
-#include
-
-#define INI_SERIALIZE_DEFAULT_INITIAL_CAPACITY 256
-
-// ===[ IniFile Types ]===
-
-typedef struct {
- char* name;
- char** keys;
- char** values;
- int count;
- int capacity;
-} IniSection;
-
-typedef struct {
- IniSection* sections;
- int count;
- int capacity;
-} IniFile;
-
-// ===[ Lifecycle ]===
-
-IniFile* Ini_parse(const char* text);
-void Ini_free(IniFile* ini);
-
-// ===[ Queries ]===
-
-const char* Ini_getString(const IniFile* ini, const char* section, const char* key);
-bool Ini_hasSection(const IniFile* ini, const char* section);
-bool Ini_hasKey(const IniFile* ini, const char* section, const char* key);
-
-// ===[ Mutation ]===
-
-void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value);
-void Ini_deleteKey(IniFile* ini, const char* section, const char* key);
-void Ini_deleteSection(IniFile* ini, const char* section);
-
-// ===[ Serialization ]===
-
-char* Ini_serialize(const IniFile* ini, size_t initialCapacity);
+#pragma once
+
+#include "common.h"
+#include
+
+#define INI_SERIALIZE_DEFAULT_INITIAL_CAPACITY 256
+
+// ===[ IniFile Types ]===
+
+typedef struct {
+ char* name;
+ char** keys;
+ char** values;
+ int count;
+ int capacity;
+} IniSection;
+
+typedef struct {
+ IniSection* sections;
+ int count;
+ int capacity;
+} IniFile;
+
+// ===[ Lifecycle ]===
+
+IniFile* Ini_parse(const char* text);
+void Ini_free(IniFile* ini);
+
+// ===[ Queries ]===
+
+const char* Ini_getString(const IniFile* ini, const char* section, const char* key);
+bool Ini_hasSection(const IniFile* ini, const char* section);
+bool Ini_hasKey(const IniFile* ini, const char* section, const char* key);
+
+// ===[ Mutation ]===
+
+void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value);
+void Ini_deleteKey(IniFile* ini, const char* section, const char* key);
+void Ini_deleteSection(IniFile* ini, const char* section);
+
+// ===[ Serialization ]===
+
+char* Ini_serialize(const IniFile* ini, size_t initialCapacity);
diff --git a/src/input_recording.c b/src/input_recording.c
index 8e69432d..c8566f2e 100644
--- a/src/input_recording.c
+++ b/src/input_recording.c
@@ -1,213 +1,213 @@
-#include "input_recording.h"
-#include "json_reader.h"
-#include "json_writer.h"
-
-#include "utils.h"
-
-#include
-#include
-#include
-
-#include "stb_ds.h"
-
-InputRecording* InputRecording_createRecorder(const char* filePath) {
- InputRecording* rec = safeCalloc(1, sizeof(InputRecording));
- rec->isRecording = true;
- rec->recordFilePath = filePath;
- return rec;
-}
-
-InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath) {
- // Read the file contents
- FILE* f = fopen(playbackFilePath, "r");
- if (f == nullptr) {
- fprintf(stderr, "Error: Could not open input recording file '%s'\n", playbackFilePath);
- exit(1);
- }
-
- fseek(f, 0, SEEK_END);
- long fileSize = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- char* contents = safeMalloc(fileSize + 1);
- fread(contents, 1, fileSize, f);
- contents[fileSize] = '\0';
- fclose(f);
-
- // Parse JSON
- JsonValue* root = JsonReader_parse(contents);
- free(contents);
-
- if (root == nullptr || !JsonReader_isObject(root)) {
- fprintf(stderr, "Error: Invalid JSON in input recording file '%s'\n", playbackFilePath);
- exit(1);
- }
-
- // Find the highest frame number to determine array size
- int objectLen = JsonReader_objectLength(root);
- int32_t maxFrame = -1;
- repeat(objectLen, i) {
- const char* key = JsonReader_getObjectKey(root, i);
- int32_t frameNum = (int32_t) strtol(key, nullptr, 10);
- if (frameNum > maxFrame) maxFrame = frameNum;
- }
-
- InputRecording* rec = safeCalloc(1, sizeof(InputRecording));
- rec->isPlayback = true;
- rec->playbackFrameCount = maxFrame + 1;
-
- // If a record path was provided, also enable recording
- if (recordFilePath != nullptr) {
- rec->isRecording = true;
- rec->recordFilePath = recordFilePath;
- }
-
- // Allocate playbackFrames array (one stb_ds int32_t array per frame)
- rec->playbackFrames = safeCalloc(rec->playbackFrameCount, sizeof(int32_t*));
-
- repeat(objectLen, i) {
- const char* key = JsonReader_getObjectKey(root, i);
- JsonValue* val = JsonReader_getObjectValue(root, i);
- int32_t frameNum = (int32_t) strtol(key, nullptr, 10);
-
- if (JsonReader_isArray(val)) {
- int keyCount = JsonReader_arrayLength(val);
- int32_t* keys = nullptr;
- repeat(keyCount, k) {
- JsonValue* keyVal = JsonReader_getArrayElement(val, k);
- arrput(keys, (int32_t) JsonReader_getInt(keyVal));
- }
- rec->playbackFrames[frameNum] = keys;
- }
- }
-
- JsonReader_free(root);
- fprintf(stderr, "InputRecording: Loaded %d frames from '%s'\n", rec->playbackFrameCount, playbackFilePath);
- return rec;
-}
-
-void InputRecording_free(InputRecording* recording) {
- if (recording == nullptr) return;
-
- if (recording->recordedFrames != nullptr) {
- int32_t count = (int32_t) arrlen(recording->recordedFrames);
- repeat(count, i) {
- arrfree(recording->recordedFrames[i]);
- }
- arrfree(recording->recordedFrames);
- }
-
- if (recording->playbackFrames != nullptr) {
- repeat(recording->playbackFrameCount, i) {
- arrfree(recording->playbackFrames[i]);
- }
- free(recording->playbackFrames);
- }
-
- free(recording);
-}
-
-void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber) {
- if (recording == nullptr) return;
-
- // Playback: overwrite keyboard state from recorded data (while frames remain)
- if (recording->isPlayback) {
- if (recording->playbackFrameCount > frameNumber) {
- int32_t* frameKeys = recording->playbackFrames[frameNumber];
- int32_t keyCount = (int32_t) arrlen(frameKeys);
-
- // Build a temporary "current held" array for this frame
- bool currentKeyDown[GML_KEY_COUNT];
- memset(currentKeyDown, 0, sizeof(currentKeyDown));
- repeat(keyCount, i) {
- int32_t key = frameKeys[i];
- if (GML_KEY_COUNT > key && key >= 0) {
- currentKeyDown[key] = true;
- }
- }
-
- // Derive transitions by comparing against previousKeyDown
- repeat(GML_KEY_COUNT, key) {
- kb->keyDown[key] = currentKeyDown[key];
- kb->keyPressed[key] = currentKeyDown[key] && !recording->previousKeyDown[key];
- kb->keyReleased[key] = !currentKeyDown[key] && recording->previousKeyDown[key];
- if (kb->keyPressed[key]) {
- kb->lastKey = (int32_t) key;
- }
- }
-
- memcpy(recording->previousKeyDown, currentKeyDown, sizeof(currentKeyDown));
- } else {
- // Past the end of recorded data: release everything, then let real input through
- if (!recording->playbackEnded) {
- fprintf(stderr, "InputRecording: Playback ended at frame %d (recorded %d frames)\n", frameNumber, recording->playbackFrameCount);
- recording->playbackEnded = true;
-
- repeat(GML_KEY_COUNT, key) {
- kb->keyReleased[key] = recording->previousKeyDown[key];
- kb->keyDown[key] = false;
- kb->keyPressed[key] = false;
- }
- memset(recording->previousKeyDown, 0, sizeof(recording->previousKeyDown));
- }
- // After the first "ended" frame, real keyboard input flows through naturally
- }
- }
-
- // Recording: snapshot whatever the current keyboard state is (from real input or playback)
- if (recording->isRecording) {
- int32_t* heldKeys = nullptr;
- repeat(GML_KEY_COUNT, key) {
- if (kb->keyDown[key]) {
- arrput(heldKeys, (int32_t) key);
- }
- }
- arrput(recording->recordedFrames, heldKeys);
- }
-}
-
-bool InputRecording_save(InputRecording* recording) {
- if (recording == nullptr || !recording->isRecording) return false;
-
- int32_t frameCount = (int32_t) arrlen(recording->recordedFrames);
-
- JsonWriter w = JsonWriter_create();
- JsonWriter_beginObject(&w);
-
- repeat(frameCount, f) {
- // Frame number as string key
- char frameKey[16];
- snprintf(frameKey, sizeof(frameKey), "%d", (int) f);
- JsonWriter_key(&w, frameKey);
-
- JsonWriter_beginArray(&w);
- int32_t* keys = recording->recordedFrames[f];
- int32_t keyCount = (int32_t) arrlen(keys);
- repeat(keyCount, k) {
- JsonWriter_int(&w, keys[k]);
- }
- JsonWriter_endArray(&w);
- }
-
- JsonWriter_endObject(&w);
-
- FILE* f = fopen(recording->recordFilePath, "w");
- if (f == nullptr) {
- fprintf(stderr, "Error: Could not write input recording to '%s'\n", recording->recordFilePath);
- JsonWriter_free(&w);
- return false;
- }
-
- const char* output = JsonWriter_getOutput(&w);
- fwrite(output, 1, JsonWriter_getLength(&w), f);
- fputc('\n', f);
- fclose(f);
-
- fprintf(stderr, "InputRecording: Saved %d frames to '%s'\n", frameCount, recording->recordFilePath);
- JsonWriter_free(&w);
- return true;
-}
-
-bool InputRecording_isPlaybackActive(InputRecording* recording) {
- return recording != nullptr && recording->isPlayback && !recording->playbackEnded;
-}
+#include "input_recording.h"
+#include "json_reader.h"
+#include "json_writer.h"
+
+#include "utils.h"
+
+#include
+#include
+#include
+
+#include
+
+InputRecording* InputRecording_createRecorder(const char* filePath) {
+ InputRecording* rec = safeCalloc(1, sizeof(InputRecording));
+ rec->isRecording = true;
+ rec->recordFilePath = filePath;
+ return rec;
+}
+
+InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath) {
+ // Read the file contents
+ FILE* f = fopen(playbackFilePath, "r");
+ if (f == nullptr) {
+ fprintf(stderr, "Error: Could not open input recording file '%s'\n", playbackFilePath);
+ exit(1);
+ }
+
+ fseek(f, 0, SEEK_END);
+ long fileSize = ftell(f);
+ fseek(f, 0, SEEK_SET);
+
+ char* contents = safeMalloc(fileSize + 1);
+ fread(contents, 1, fileSize, f);
+ contents[fileSize] = '\0';
+ fclose(f);
+
+ // Parse JSON
+ JsonValue* root = JsonReader_parse(contents);
+ free(contents);
+
+ if (root == nullptr || !JsonReader_isObject(root)) {
+ fprintf(stderr, "Error: Invalid JSON in input recording file '%s'\n", playbackFilePath);
+ exit(1);
+ }
+
+ // Find the highest frame number to determine array size
+ int objectLen = JsonReader_objectLength(root);
+ int32_t maxFrame = -1;
+ repeat(objectLen, i) {
+ const char* key = JsonReader_getObjectKey(root, i);
+ int32_t frameNum = (int32_t) strtol(key, nullptr, 10);
+ if (frameNum > maxFrame) maxFrame = frameNum;
+ }
+
+ InputRecording* rec = safeCalloc(1, sizeof(InputRecording));
+ rec->isPlayback = true;
+ rec->playbackFrameCount = maxFrame + 1;
+
+ // If a record path was provided, also enable recording
+ if (recordFilePath != nullptr) {
+ rec->isRecording = true;
+ rec->recordFilePath = recordFilePath;
+ }
+
+ // Allocate playbackFrames array (one stb_ds int32_t array per frame)
+ rec->playbackFrames = safeCalloc(rec->playbackFrameCount, sizeof(int32_t*));
+
+ repeat(objectLen, i) {
+ const char* key = JsonReader_getObjectKey(root, i);
+ JsonValue* val = JsonReader_getObjectValue(root, i);
+ int32_t frameNum = (int32_t) strtol(key, nullptr, 10);
+
+ if (JsonReader_isArray(val)) {
+ int keyCount = JsonReader_arrayLength(val);
+ int32_t* keys = nullptr;
+ repeat(keyCount, k) {
+ JsonValue* keyVal = JsonReader_getArrayElement(val, k);
+ arrput(keys, (int32_t) JsonReader_getInt(keyVal));
+ }
+ rec->playbackFrames[frameNum] = keys;
+ }
+ }
+
+ JsonReader_free(root);
+ fprintf(stderr, "InputRecording: Loaded %d frames from '%s'\n", rec->playbackFrameCount, playbackFilePath);
+ return rec;
+}
+
+void InputRecording_free(InputRecording* recording) {
+ if (recording == nullptr) return;
+
+ if (recording->recordedFrames != nullptr) {
+ int32_t count = (int32_t) arrlen(recording->recordedFrames);
+ repeat(count, i) {
+ arrfree(recording->recordedFrames[i]);
+ }
+ arrfree(recording->recordedFrames);
+ }
+
+ if (recording->playbackFrames != nullptr) {
+ repeat(recording->playbackFrameCount, i) {
+ arrfree(recording->playbackFrames[i]);
+ }
+ free(recording->playbackFrames);
+ }
+
+ free(recording);
+}
+
+void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber) {
+ if (recording == nullptr) return;
+
+ // Playback: overwrite keyboard state from recorded data (while frames remain)
+ if (recording->isPlayback) {
+ if (recording->playbackFrameCount > frameNumber) {
+ int32_t* frameKeys = recording->playbackFrames[frameNumber];
+ int32_t keyCount = (int32_t) arrlen(frameKeys);
+
+ // Build a temporary "current held" array for this frame
+ bool currentKeyDown[GML_KEY_COUNT];
+ memset(currentKeyDown, 0, sizeof(currentKeyDown));
+ repeat(keyCount, i) {
+ int32_t key = frameKeys[i];
+ if (GML_KEY_COUNT > key && key >= 0) {
+ currentKeyDown[key] = true;
+ }
+ }
+
+ // Derive transitions by comparing against previousKeyDown
+ repeat(GML_KEY_COUNT, key) {
+ kb->keyDown[key] = currentKeyDown[key];
+ kb->keyPressed[key] = currentKeyDown[key] && !recording->previousKeyDown[key];
+ kb->keyReleased[key] = !currentKeyDown[key] && recording->previousKeyDown[key];
+ if (kb->keyPressed[key]) {
+ kb->lastKey = (int32_t) key;
+ }
+ }
+
+ memcpy(recording->previousKeyDown, currentKeyDown, sizeof(currentKeyDown));
+ } else {
+ // Past the end of recorded data: release everything, then let real input through
+ if (!recording->playbackEnded) {
+ fprintf(stderr, "InputRecording: Playback ended at frame %d (recorded %d frames)\n", frameNumber, recording->playbackFrameCount);
+ recording->playbackEnded = true;
+
+ repeat(GML_KEY_COUNT, key) {
+ kb->keyReleased[key] = recording->previousKeyDown[key];
+ kb->keyDown[key] = false;
+ kb->keyPressed[key] = false;
+ }
+ memset(recording->previousKeyDown, 0, sizeof(recording->previousKeyDown));
+ }
+ // After the first "ended" frame, real keyboard input flows through naturally
+ }
+ }
+
+ // Recording: snapshot whatever the current keyboard state is (from real input or playback)
+ if (recording->isRecording) {
+ int32_t* heldKeys = nullptr;
+ repeat(GML_KEY_COUNT, key) {
+ if (kb->keyDown[key]) {
+ arrput(heldKeys, (int32_t) key);
+ }
+ }
+ arrput(recording->recordedFrames, heldKeys);
+ }
+}
+
+bool InputRecording_save(InputRecording* recording) {
+ if (recording == nullptr || !recording->isRecording) return false;
+
+ int32_t frameCount = (int32_t) arrlen(recording->recordedFrames);
+
+ JsonWriter w = JsonWriter_create();
+ JsonWriter_beginObject(&w);
+
+ repeat(frameCount, f) {
+ // Frame number as string key
+ char frameKey[16];
+ snprintf(frameKey, sizeof(frameKey), "%d", (int) f);
+ JsonWriter_key(&w, frameKey);
+
+ JsonWriter_beginArray(&w);
+ int32_t* keys = recording->recordedFrames[f];
+ int32_t keyCount = (int32_t) arrlen(keys);
+ repeat(keyCount, k) {
+ JsonWriter_int(&w, keys[k]);
+ }
+ JsonWriter_endArray(&w);
+ }
+
+ JsonWriter_endObject(&w);
+
+ FILE* f = fopen(recording->recordFilePath, "w");
+ if (f == nullptr) {
+ fprintf(stderr, "Error: Could not write input recording to '%s'\n", recording->recordFilePath);
+ JsonWriter_free(&w);
+ return false;
+ }
+
+ const char* output = JsonWriter_getOutput(&w);
+ fwrite(output, 1, JsonWriter_getLength(&w), f);
+ fputc('\n', f);
+ fclose(f);
+
+ fprintf(stderr, "InputRecording: Saved %d frames to '%s'\n", frameCount, recording->recordFilePath);
+ JsonWriter_free(&w);
+ return true;
+}
+
+bool InputRecording_isPlaybackActive(InputRecording* recording) {
+ return recording != nullptr && recording->isPlayback && !recording->playbackEnded;
+}
diff --git a/src/input_recording.h b/src/input_recording.h
index 100648fe..b57ef2bb 100644
--- a/src/input_recording.h
+++ b/src/input_recording.h
@@ -1,40 +1,40 @@
-#pragma once
-
-#include "common.h"
-#include
-
-#include "runner_keyboard.h"
-
-typedef struct InputRecording {
- bool isRecording;
- bool isPlayback;
- const char* recordFilePath;
-
- // Recording: stb_ds array of stb_ds int32_t arrays (one per frame)
- int32_t** recordedFrames;
-
- // Playback: same structure, loaded from JSON
- int32_t** playbackFrames;
- int32_t playbackFrameCount;
- bool playbackEnded;
- bool previousKeyDown[GML_KEY_COUNT];
-} InputRecording;
-
-// Create a recorder that snapshots keyboard state each frame
-InputRecording* InputRecording_createRecorder(const char* filePath);
-
-// Create a player that loads recorded input from a JSON file.
-// If recordFilePath is non-null, also enables recording (playback + record mode).
-InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath);
-
-// Free all resources
-void InputRecording_free(InputRecording* recording);
-
-// Called each frame: plays back recorded input (if active), then snapshots keyboard state (if recording)
-void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber);
-
-// Write recorded frames to the JSON file (returns true on success)
-bool InputRecording_save(InputRecording* recording);
-
-// Null-safe check: returns true if recording is non-null and playback hasn't ended yet
-bool InputRecording_isPlaybackActive(InputRecording* recording);
+#pragma once
+
+#include "common.h"
+#include
+
+#include "runner_keyboard.h"
+
+typedef struct InputRecording {
+ bool isRecording;
+ bool isPlayback;
+ const char* recordFilePath;
+
+ // Recording: stb_ds array of stb_ds int32_t arrays (one per frame)
+ int32_t** recordedFrames;
+
+ // Playback: same structure, loaded from JSON
+ int32_t** playbackFrames;
+ int32_t playbackFrameCount;
+ bool playbackEnded;
+ bool previousKeyDown[GML_KEY_COUNT];
+} InputRecording;
+
+// Create a recorder that snapshots keyboard state each frame
+InputRecording* InputRecording_createRecorder(const char* filePath);
+
+// Create a player that loads recorded input from a JSON file.
+// If recordFilePath is non-null, also enables recording (playback + record mode).
+InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath);
+
+// Free all resources
+void InputRecording_free(InputRecording* recording);
+
+// Called each frame: plays back recorded input (if active), then snapshots keyboard state (if recording)
+void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber);
+
+// Write recorded frames to the JSON file (returns true on success)
+bool InputRecording_save(InputRecording* recording);
+
+// Null-safe check: returns true if recording is non-null and playback hasn't ended yet
+bool InputRecording_isPlaybackActive(InputRecording* recording);
diff --git a/src/instance.c b/src/instance.c
index f7fcef6c..ca83fa90 100644
--- a/src/instance.c
+++ b/src/instance.c
@@ -1,182 +1,188 @@
-#include "instance.h"
-
-#include
-#include
-#include
-
-#include "stb_ds.h"
-#include "utils.h"
-#include "int_rvalue_hashmap.h"
-
-Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y) {
- Instance* inst = safeCalloc(1, sizeof(Instance));
- inst->instanceId = instanceId;
- inst->objectIndex = objectIndex;
- inst->refCount = 0;
- inst->structRegistryIndex = -1;
- inst->x = (float) x;
- inst->y = (float) y;
- inst->xprevious = (float) x;
- inst->yprevious = (float) y;
- inst->xstart = (float) x;
- inst->ystart = (float) y;
- inst->maskIndex = -1;
- inst->persistent = false;
- inst->solid = false;
- inst->active = true;
- inst->visible = true;
- inst->destroyed = false;
+#include "instance.h"
+
+#include
+#include
+#include
+
+#include "stb_ds.h"
+#include "utils.h"
+#include "int_rvalue_hashmap.h"
+
+Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y) {
+ Instance* inst = safeCalloc(1, sizeof(Instance));
+ inst->instanceId = instanceId;
+ inst->objectIndex = objectIndex;
+ inst->refCount = 0;
+ inst->structRegistryIndex = -1;
+ inst->x = (float) x;
+ inst->y = (float) y;
+ inst->xprevious = (float) x;
+ inst->yprevious = (float) y;
+ inst->xstart = (float) x;
+ inst->ystart = (float) y;
+ inst->maskIndex = -1;
+ inst->persistent = false;
+ inst->solid = false;
+ inst->active = true;
+ inst->visible = true;
+ inst->destroyed = false;
inst->outsideRoom = false;
inst->spatialGridDirty = false;
inst->spriteIndex = -1;
+ inst->cachedDrawSpriteIndex = -1;
+ inst->cachedDrawSubimg = INT32_MIN;
+ inst->cachedDrawTPAGIndex = -1;
inst->imageSpeed = 1.0f;
- inst->imageIndex = 0.0f;
- inst->imageXscale = 1.0f;
- inst->imageYscale = 1.0f;
- inst->imageAngle = 0.0f;
- inst->imageAlpha = 1.0f;
- inst->imageBlend = 0xFFFFFF;
- inst->depth = 0;
- inst->layer = -1;
- inst->speed = 0.0f;
- inst->direction = 0.0f;
- inst->hspeed = 0.0f;
- inst->vspeed = 0.0f;
- inst->friction = 0.0f;
- inst->gravity = 0.0f;
- inst->gravityDirection = 270.0f;
- inst->pathIndex = -1;
- inst->pathScale = 1.0f;
-
- // Initialize alarms to -1 (inactive)
- repeat(GML_ALARM_COUNT, i) {
- inst->alarm[i] = -1;
- }
-
- return inst;
-}
-
-void Instance_structIncRef(Instance* inst) {
- if (inst == nullptr) return;
- inst->refCount++;
-}
-
-void Instance_structDecRef(Instance* inst) {
- if (inst == nullptr) return;
- require(inst->refCount > 0);
- inst->refCount--;
- // Never free here. The runner-side sweep reaps structs whose refCount has dropped to 1. (that is: when only the structInstances holds it)
-}
-
-uint32_t Instance_getInstanceId(Instance* inst) {
- return inst != nullptr ? inst->instanceId : 0;
-}
-
-void Instance_free(Instance* instance) {
- if (instance == nullptr) return;
-
- // Free owned strings and decRef owned arrays in selfVars hashmap, then release the entries buffer.
- IntRValueHashMap_freeAllValues(&instance->selfVars);
- arrfree(instance->collisionCells);
-
- free(instance);
-}
-
-void Instance_copyFields(Instance* source, Instance* destination) {
- destination->x = source->x;
- destination->y = source->y;
- destination->xprevious = source->xprevious;
- destination->yprevious = source->yprevious;
- destination->xstart = source->xstart;
- destination->ystart = source->ystart;
- destination->persistent = source->persistent;
- destination->solid = source->solid;
- destination->active = source->active;
- destination->visible = source->visible;
+ inst->imageIndex = 0.0f;
+ inst->imageXscale = 1.0f;
+ inst->imageYscale = 1.0f;
+ inst->imageAngle = 0.0f;
+ inst->imageAlpha = 1.0f;
+ inst->imageBlend = 0xFFFFFF;
+ inst->depth = 0;
+ inst->layer = -1;
+ inst->speed = 0.0f;
+ inst->direction = 0.0f;
+ inst->hspeed = 0.0f;
+ inst->vspeed = 0.0f;
+ inst->friction = 0.0f;
+ inst->gravity = 0.0f;
+ inst->gravityDirection = 270.0f;
+ inst->pathIndex = -1;
+ inst->pathScale = 1.0f;
+
+ // Initialize alarms to -1 (inactive)
+ repeat(GML_ALARM_COUNT, i) {
+ inst->alarm[i] = -1;
+ }
+
+ return inst;
+}
+
+void Instance_structIncRef(Instance* inst) {
+ if (inst == nullptr) return;
+ inst->refCount++;
+}
+
+void Instance_structDecRef(Instance* inst) {
+ if (inst == nullptr) return;
+ require(inst->refCount > 0);
+ inst->refCount--;
+ // Never free here. The runner-side sweep reaps structs whose refCount has dropped to 1. (that is: when only the structInstances holds it)
+}
+
+uint32_t Instance_getInstanceId(Instance* inst) {
+ return inst != nullptr ? inst->instanceId : 0;
+}
+
+void Instance_free(Instance* instance) {
+ if (instance == nullptr) return;
+
+ // Free owned strings and decRef owned arrays in selfVars hashmap, then release the entries buffer.
+ IntRValueHashMap_freeAllValues(&instance->selfVars);
+ arrfree(instance->collisionCells);
+
+ free(instance);
+}
+
+void Instance_copyFields(Instance* source, Instance* destination) {
+ destination->x = source->x;
+ destination->y = source->y;
+ destination->xprevious = source->xprevious;
+ destination->yprevious = source->yprevious;
+ destination->xstart = source->xstart;
+ destination->ystart = source->ystart;
+ destination->persistent = source->persistent;
+ destination->solid = source->solid;
+ destination->active = source->active;
+ destination->visible = source->visible;
destination->outsideRoom = source->outsideRoom;
destination->maskIndex = source->maskIndex;
destination->spriteIndex = source->spriteIndex;
+ destination->cachedDrawSpriteIndex = source->cachedDrawSpriteIndex;
+ destination->cachedDrawSubimg = source->cachedDrawSubimg;
+ destination->cachedDrawTPAGIndex = source->cachedDrawTPAGIndex;
destination->imageSpeed = source->imageSpeed;
- destination->imageIndex = source->imageIndex;
- destination->imageXscale = source->imageXscale;
- destination->imageYscale = source->imageYscale;
- destination->imageAngle = source->imageAngle;
- destination->imageAlpha = source->imageAlpha;
- destination->imageBlend = source->imageBlend;
- destination->depth = source->depth;
- destination->layer = source->layer;
- destination->speed = source->speed;
- destination->direction = source->direction;
- destination->hspeed = source->hspeed;
- destination->vspeed = source->vspeed;
- destination->friction = source->friction;
- destination->gravity = source->gravity;
- destination->gravityDirection = source->gravityDirection;
- destination->pathIndex = source->pathIndex;
- destination->pathPosition = source->pathPosition;
- destination->pathPositionPrevious = source->pathPositionPrevious;
- destination->pathSpeed = source->pathSpeed;
- destination->pathScale = source->pathScale;
- destination->pathOrientation = source->pathOrientation;
- destination->pathEndAction = source->pathEndAction;
- destination->pathXStart = source->pathXStart;
- destination->pathYStart = source->pathYStart;
- repeat(GML_ALARM_COUNT, i) {
- destination->alarm[i] = source->alarm[i];
- }
- destination->activeAlarmMask = source->activeAlarmMask;
-
- // Deep-copy self variables (Instance_setSelfVar handles string duplication + array incRef)
- repeat(source->selfVars.capacity, i) {
- IntRValueEntry* entry = &source->selfVars.entries[i];
- if (entry->key != INT_RVALUE_HASHMAP_EMPTY_KEY) {
- Instance_setSelfVar(destination, entry->key, entry->value);
- }
- }
-}
-
-// Compute speed and direction from hspeed/vspeed (HTML5: Compute_Speed1)
-void Instance_computeSpeedFromComponents(Instance* inst) {
- // Direction
- if (inst->hspeed == 0.0f) {
- if (inst->vspeed > 0.0f) {
- inst->direction = 270.0f;
- } else if (inst->vspeed < 0.0f) {
- inst->direction = 90.0f;
- }
- // If both are 0, direction stays unchanged
- } else {
- GMLReal dd = clampFloat(180.0 * GMLReal_atan2(inst->vspeed, inst->hspeed) / M_PI);
- if (dd <= 0.0) {
- inst->direction = (float) -dd;
- } else {
- inst->direction = (float) (360.0 - dd);
- }
- }
-
- // Round direction if very close to integer
- if (GMLReal_fabs(inst->direction - GMLReal_round(inst->direction)) < 0.0001) {
- inst->direction = (float) GMLReal_round(inst->direction);
- }
- inst->direction = (float) GMLReal_fmod(inst->direction, 360.0);
-
- // Speed
- inst->speed = (float) GMLReal_sqrt(inst->hspeed * inst->hspeed + inst->vspeed * inst->vspeed);
- if (GMLReal_fabs(inst->speed - GMLReal_round(inst->speed)) < 0.0001) {
- inst->speed = (float) GMLReal_round(inst->speed);
- }
-}
-
-// Compute hspeed/vspeed from speed and direction (HTML5: Compute_Speed2)
-void Instance_computeComponentsFromSpeed(Instance* inst) {
- inst->hspeed = (float) (inst->speed * clampFloat(GMLReal_cos(inst->direction * (M_PI / 180.0))));
- inst->vspeed = (float) (-inst->speed * clampFloat(GMLReal_sin(inst->direction * (M_PI / 180.0))));
-
- // Round if very close to integer
- if (GMLReal_fabs(inst->hspeed - GMLReal_round(inst->hspeed)) < 0.0001) {
- inst->hspeed = (float) GMLReal_round(inst->hspeed);
- }
- if (GMLReal_fabs(inst->vspeed - GMLReal_round(inst->vspeed)) < 0.0001) {
- inst->vspeed = (float) GMLReal_round(inst->vspeed);
- }
-}
+ destination->imageIndex = source->imageIndex;
+ destination->imageXscale = source->imageXscale;
+ destination->imageYscale = source->imageYscale;
+ destination->imageAngle = source->imageAngle;
+ destination->imageAlpha = source->imageAlpha;
+ destination->imageBlend = source->imageBlend;
+ destination->depth = source->depth;
+ destination->layer = source->layer;
+ destination->speed = source->speed;
+ destination->direction = source->direction;
+ destination->hspeed = source->hspeed;
+ destination->vspeed = source->vspeed;
+ destination->friction = source->friction;
+ destination->gravity = source->gravity;
+ destination->gravityDirection = source->gravityDirection;
+ destination->pathIndex = source->pathIndex;
+ destination->pathPosition = source->pathPosition;
+ destination->pathPositionPrevious = source->pathPositionPrevious;
+ destination->pathSpeed = source->pathSpeed;
+ destination->pathScale = source->pathScale;
+ destination->pathOrientation = source->pathOrientation;
+ destination->pathEndAction = source->pathEndAction;
+ destination->pathXStart = source->pathXStart;
+ destination->pathYStart = source->pathYStart;
+ repeat(GML_ALARM_COUNT, i) {
+ destination->alarm[i] = source->alarm[i];
+ }
+ destination->activeAlarmMask = source->activeAlarmMask;
+
+ // Deep-copy self variables (Instance_setSelfVar handles string duplication + array incRef)
+ repeat(source->selfVars.capacity, i) {
+ IntRValueEntry* entry = &source->selfVars.entries[i];
+ if (entry->key != INT_RVALUE_HASHMAP_EMPTY_KEY) {
+ Instance_setSelfVar(destination, entry->key, entry->value);
+ }
+ }
+}
+
+// Compute speed and direction from hspeed/vspeed (HTML5: Compute_Speed1)
+void Instance_computeSpeedFromComponents(Instance* inst) {
+ // Direction
+ if (inst->hspeed == 0.0f) {
+ if (inst->vspeed > 0.0f) {
+ inst->direction = 270.0f;
+ } else if (inst->vspeed < 0.0f) {
+ inst->direction = 90.0f;
+ }
+ // If both are 0, direction stays unchanged
+ } else {
+ GMLReal dd = clampFloat(180.0 * GMLReal_atan2(inst->vspeed, inst->hspeed) / M_PI);
+ if (dd <= 0.0) {
+ inst->direction = (float) -dd;
+ } else {
+ inst->direction = (float) (360.0 - dd);
+ }
+ }
+
+ // Round direction if very close to integer
+ if (GMLReal_fabs(inst->direction - GMLReal_round(inst->direction)) < 0.0001) {
+ inst->direction = (float) GMLReal_round(inst->direction);
+ }
+ inst->direction = (float) GMLReal_fmod(inst->direction, 360.0);
+
+ // Speed
+ inst->speed = (float) GMLReal_sqrt(inst->hspeed * inst->hspeed + inst->vspeed * inst->vspeed);
+ if (GMLReal_fabs(inst->speed - GMLReal_round(inst->speed)) < 0.0001) {
+ inst->speed = (float) GMLReal_round(inst->speed);
+ }
+}
+
+// Compute hspeed/vspeed from speed and direction (HTML5: Compute_Speed2)
+void Instance_computeComponentsFromSpeed(Instance* inst) {
+ inst->hspeed = (float) (inst->speed * clampFloat(GMLReal_cos(inst->direction * (M_PI / 180.0))));
+ inst->vspeed = (float) (-inst->speed * clampFloat(GMLReal_sin(inst->direction * (M_PI / 180.0))));
+
+ // Round if very close to integer
+ if (GMLReal_fabs(inst->hspeed - GMLReal_round(inst->hspeed)) < 0.0001) {
+ inst->hspeed = (float) GMLReal_round(inst->hspeed);
+ }
+ if (GMLReal_fabs(inst->vspeed - GMLReal_round(inst->vspeed)) < 0.0001) {
+ inst->vspeed = (float) GMLReal_round(inst->vspeed);
+ }
+}
diff --git a/src/instance.h b/src/instance.h
index 11d3397c..5fcba4da 100644
--- a/src/instance.h
+++ b/src/instance.h
@@ -1,111 +1,114 @@
-#pragma once
-
-#include "common.h"
-#include
-#include "rvalue.h"
-#include "gml_array.h"
-#include "int_rvalue_hashmap.h"
-
-#define GML_ALARM_COUNT 12
-
-// Forward decl for Instance_structDecRef
-struct Runner;
-
-typedef struct Instance {
- uint32_t instanceId;
- int32_t objectIndex;
- // Reference count for GML structs (objectIndex == -1 mode). Unused for game-object instances.
- // The runner's structInstances registry holds an implicit +1 ref while the struct is registered, so a refCount of 1 means "only the registry references this"; the per-frame sweep (Runner_sweepDeadStructs) decRefs those to free them. RValues with ownsReference=true on RVALUE_STRUCT contribute one ref each.
- int32_t refCount;
- // Position of this struct in runner->structInstances (for O(1) swap-remove when freed). -1 when not registered.
- int32_t structRegistryIndex;
- // Native GMS runner stores all instance built-in variables as float (32-bit),
- // even though RValues use double. This matches the native precision model.
- float x, y;
- float xprevious, yprevious;
- float xstart, ystart;
- bool persistent, solid, active, destroyed, visible, createEventFired, outsideRoom, spatialGridDirty;
- // Used to track which alarms are set without looping through the entire alarm array
- uint16_t activeAlarmMask;
- int32_t maskIndex; // collision mask sprite override (-1 = use spriteIndex)
- int32_t* collisionCells; // Used to track where we are
- uint32_t lastCollisionQueryId;
-
- // Per-instance self variable storage (sparse open-addressed hashmap, keyed by varID).
- IntRValueHashMap selfVars;
-
+#pragma once
+
+#include "common.h"
+#include
+#include "rvalue.h"
+#include "gml_array.h"
+#include "int_rvalue_hashmap.h"
+
+#define GML_ALARM_COUNT 12
+
+// Forward decl for Instance_structDecRef
+struct Runner;
+
+typedef struct Instance {
+ uint32_t instanceId;
+ int32_t objectIndex;
+ // Reference count for GML structs (objectIndex == -1 mode). Unused for game-object instances.
+ // The runner's structInstances registry holds an implicit +1 ref while the struct is registered, so a refCount of 1 means "only the registry references this"; the per-frame sweep (Runner_sweepDeadStructs) decRefs those to free them. RValues with ownsReference=true on RVALUE_STRUCT contribute one ref each.
+ int32_t refCount;
+ // Position of this struct in runner->structInstances (for O(1) swap-remove when freed). -1 when not registered.
+ int32_t structRegistryIndex;
+ // Native GMS runner stores all instance built-in variables as float (32-bit),
+ // even though RValues use double. This matches the native precision model.
+ float x, y;
+ float xprevious, yprevious;
+ float xstart, ystart;
+ bool persistent, solid, active, destroyed, visible, createEventFired, outsideRoom, spatialGridDirty;
+ // Used to track which alarms are set without looping through the entire alarm array
+ uint16_t activeAlarmMask;
+ int32_t maskIndex; // collision mask sprite override (-1 = use spriteIndex)
+ int32_t* collisionCells; // Used to track where we are
+ uint32_t lastCollisionQueryId;
+
+ // Per-instance self variable storage (sparse open-addressed hashmap, keyed by varID).
+ IntRValueHashMap selfVars;
+
// Built-in instance properties
int32_t spriteIndex;
+ int32_t cachedDrawSpriteIndex;
+ int32_t cachedDrawSubimg;
+ int32_t cachedDrawTPAGIndex;
float imageSpeed, imageIndex;
- float imageXscale, imageYscale, imageAngle, imageAlpha;
- uint32_t imageBlend;
- int32_t depth;
- int32_t layer;
-
- // Motion properties
- float speed, direction;
- float hspeed, vspeed;
- float friction;
- float gravity, gravityDirection;
-
- // Path following state
- int32_t pathIndex; // -1 = no path active
- float pathPosition; // 0.0-1.0
- float pathPositionPrevious;
- float pathSpeed;
- float pathScale; // default 1.0
- float pathOrientation; // degrees, default 0.0
- int32_t pathEndAction; // 0=stop, 1=restart, 2=continue, 3=reverse
- float pathXStart; // origin for relative paths
- float pathYStart;
-
- int32_t alarm[GML_ALARM_COUNT];
-} Instance;
-
-Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y);
-void Instance_free(Instance* instance);
-
-// GML-struct refcount helpers. Only meaningful when inst->objectIndex == -1.
-// incRef: bumps the count. decRef: drops the count. Never frees on its own; the per-frame sweep (Runner_sweepDeadStructs) is the single point that physically frees a struct (after dropping the registry's implicit ref).
-void Instance_structIncRef(Instance* inst);
-void Instance_structDecRef(Instance* inst);
-
-// Deep-copy all mutable fields from source to dst: built-in properties, alarms, selfVars.
-// Does NOT copy instanceId, objectIndex, destroyed, or createEventFired. Strings are duplicated so ownership stays independent. Arrays bump refCount (shared - CoW handles forking on first write).
-void Instance_copyFields(Instance* dst, Instance* source);
-
-// Get a self variable by varID. Returns RVALUE_UNDEFINED if absent. The returned RValue is non-owning (weak view - do not RValue_free unless you incRef/strdup first to strengthen).
-static inline RValue Instance_getSelfVar(Instance* inst, int32_t varID) {
- requireNotNull(inst);
- return IntRValueHashMap_get(&inst->selfVars, varID);
-}
-
-// Set a self variable by varID. Frees the old value if present (decRefs owned arrays).
-// Always takes an independent reference: strings are strdup'd, arrays are incRef'd, regardless of whether the caller's RValue was owning.
-// The caller retains ownership of their original `val` and remains responsible for freeing it (via RValue_free) when done.
-static inline void Instance_setSelfVar(Instance* inst, int32_t varID, RValue val) {
- requireNotNull(inst);
- // One lookup: returns the existing slot, or inserts UNDEFINED and returns the new slot.
- RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varID);
- RValue_free(slot);
- if (val.type == RVALUE_STRING && val.string != nullptr) {
- val = RValue_makeOwnedString(safeStrdup(val.string));
- } else if (val.type == RVALUE_ARRAY && val.array != nullptr) {
- GMLArray_incRef(val.array);
- val.ownsReference = true;
-#if IS_BC17_OR_HIGHER_ENABLED
- } else if (val.type == RVALUE_METHOD && val.method != nullptr) {
- GMLMethod_incRef(val.method);
- val.ownsReference = true;
-#endif
- } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) {
- Instance_structIncRef(val.structInst);
- val.ownsReference = true;
- }
- *slot = val;
-}
-
-// Recompute speed/direction from hspeed/vspeed (called when hspeed or vspeed is set)
-void Instance_computeSpeedFromComponents(Instance* inst);
-// Recompute hspeed/vspeed from speed/direction (called when speed or direction is set)
-void Instance_computeComponentsFromSpeed(Instance* inst);
+ float imageXscale, imageYscale, imageAngle, imageAlpha;
+ uint32_t imageBlend;
+ int32_t depth;
+ int32_t layer;
+
+ // Motion properties
+ float speed, direction;
+ float hspeed, vspeed;
+ float friction;
+ float gravity, gravityDirection;
+
+ // Path following state
+ int32_t pathIndex; // -1 = no path active
+ float pathPosition; // 0.0-1.0
+ float pathPositionPrevious;
+ float pathSpeed;
+ float pathScale; // default 1.0
+ float pathOrientation; // degrees, default 0.0
+ int32_t pathEndAction; // 0=stop, 1=restart, 2=continue, 3=reverse
+ float pathXStart; // origin for relative paths
+ float pathYStart;
+
+ int32_t alarm[GML_ALARM_COUNT];
+} Instance;
+
+Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y);
+void Instance_free(Instance* instance);
+
+// GML-struct refcount helpers. Only meaningful when inst->objectIndex == -1.
+// incRef: bumps the count. decRef: drops the count. Never frees on its own; the per-frame sweep (Runner_sweepDeadStructs) is the single point that physically frees a struct (after dropping the registry's implicit ref).
+void Instance_structIncRef(Instance* inst);
+void Instance_structDecRef(Instance* inst);
+
+// Deep-copy all mutable fields from source to dst: built-in properties, alarms, selfVars.
+// Does NOT copy instanceId, objectIndex, destroyed, or createEventFired. Strings are duplicated so ownership stays independent. Arrays bump refCount (shared - CoW handles forking on first write).
+void Instance_copyFields(Instance* dst, Instance* source);
+
+// Get a self variable by varID. Returns RVALUE_UNDEFINED if absent. The returned RValue is non-owning (weak view - do not RValue_free unless you incRef/strdup first to strengthen).
+static inline RValue Instance_getSelfVar(Instance* inst, int32_t varID) {
+ requireNotNull(inst);
+ return IntRValueHashMap_get(&inst->selfVars, varID);
+}
+
+// Set a self variable by varID. Frees the old value if present (decRefs owned arrays).
+// Always takes an independent reference: strings are strdup'd, arrays are incRef'd, regardless of whether the caller's RValue was owning.
+// The caller retains ownership of their original `val` and remains responsible for freeing it (via RValue_free) when done.
+static inline void Instance_setSelfVar(Instance* inst, int32_t varID, RValue val) {
+ requireNotNull(inst);
+ // One lookup: returns the existing slot, or inserts UNDEFINED and returns the new slot.
+ RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varID);
+ RValue_free(slot);
+ if (val.type == RVALUE_STRING && val.string != nullptr) {
+ val = RValue_makeOwnedString(safeStrdup(val.string));
+ } else if (val.type == RVALUE_ARRAY && val.array != nullptr) {
+ GMLArray_incRef(val.array);
+ val.ownsReference = true;
+#if IS_BC17_OR_HIGHER_ENABLED
+ } else if (val.type == RVALUE_METHOD && val.method != nullptr) {
+ GMLMethod_incRef(val.method);
+ val.ownsReference = true;
+#endif
+ } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) {
+ Instance_structIncRef(val.structInst);
+ val.ownsReference = true;
+ }
+ *slot = val;
+}
+
+// Recompute speed/direction from hspeed/vspeed (called when hspeed or vspeed is set)
+void Instance_computeSpeedFromComponents(Instance* inst);
+// Recompute hspeed/vspeed from speed/direction (called when speed or direction is set)
+void Instance_computeComponentsFromSpeed(Instance* inst);
diff --git a/src/json_reader.c b/src/json_reader.c
index 18ceea23..c3fa3aa4 100644
--- a/src/json_reader.c
+++ b/src/json_reader.c
@@ -1,503 +1,503 @@
-#include "json_reader.h"
-
-#include
-#include
-#include
-
-#include "utils.h"
-
-// ===[ Parser State ]===
-
-typedef struct {
- const char* input;
- size_t position;
- size_t length;
-} JsonParser;
-
-// ===[ Internal Helpers ]===
-
-static void skipWhitespace(JsonParser* parser) {
- while (parser->position < parser->length) {
- char c = parser->input[parser->position];
- if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
- parser->position++;
- } else {
- break;
- }
- }
-}
-
-static char peek(JsonParser* parser) {
- if (parser->position >= parser->length) return '\0';
- return parser->input[parser->position];
-}
-
-static char advance(JsonParser* parser) {
- if (parser->position >= parser->length) return '\0';
- return parser->input[parser->position++];
-}
-
-static JsonValue* makeValue(JsonValueType type) {
- JsonValue* value = safeCalloc(1, sizeof(JsonValue));
- if (value == nullptr) {
- fprintf(stderr, "JsonReader: calloc failed\n");
- abort();
- }
- value->type = type;
- return value;
-}
-
-// Forward declaration for recursive parsing
-static JsonValue* parseValue(JsonParser* parser);
-
-static JsonValue* parseString(JsonParser* parser) {
- // Skip opening quote
- advance(parser);
-
- size_t capacity = 64;
- size_t length = 0;
- char* buffer = safeMalloc(capacity);
- if (buffer == nullptr) {
- fprintf(stderr, "JsonReader: malloc failed\n");
- abort();
- }
-
- while (parser->position < parser->length) {
- char c = advance(parser);
-
- if (c == '"') {
- // End of string
- buffer[length] = '\0';
- JsonValue* value = makeValue(JSON_STRING);
- value->stringValue = buffer;
- return value;
- }
-
- if (c == '\\') {
- // Escape sequence
- char escaped = advance(parser);
- switch (escaped) {
- case '"': c = '"'; break;
- case '\\': c = '\\'; break;
- case '/': c = '/'; break;
- case 'b': c = '\b'; break;
- case 'f': c = '\f'; break;
- case 'n': c = '\n'; break;
- case 'r': c = '\r'; break;
- case 't': c = '\t'; break;
- case 'u': {
- // Parse 4-digit hex code point
- char hex[5] = {0};
- repeat(4, i) {
- hex[i] = advance(parser);
- }
- unsigned long codePoint = strtoul(hex, nullptr, 16);
-
- // Encode as UTF-8
- if (128 > codePoint) {
- c = (char) codePoint;
- } else if (2048 > codePoint) {
- if (length + 2 >= capacity) {
- capacity *= 2;
- buffer = safeRealloc(buffer, capacity);
- }
- buffer[length++] = (char) (0xC0 | (codePoint >> 6));
- c = (char) (0x80 | (codePoint & 0x3F));
- } else {
- if (length + 3 >= capacity) {
- capacity *= 2;
- buffer = safeRealloc(buffer, capacity);
- }
- buffer[length++] = (char) (0xE0 | (codePoint >> 12));
- buffer[length++] = (char) (0x80 | ((codePoint >> 6) & 0x3F));
- c = (char) (0x80 | (codePoint & 0x3F));
- }
- break;
- }
- default:
- fprintf(stderr, "JsonReader: unknown escape sequence '\\%c'\n", escaped);
- free(buffer);
- return nullptr;
- }
- }
-
- // Grow buffer if needed
- if (length + 1 >= capacity) {
- capacity *= 2;
- buffer = safeRealloc(buffer, capacity);
- if (buffer == nullptr) {
- fprintf(stderr, "JsonReader: realloc failed\n");
- abort();
- }
- }
- buffer[length++] = c;
- }
-
- // Unterminated string
- fprintf(stderr, "JsonReader: unterminated string\n");
- free(buffer);
- return nullptr;
-}
-
-static JsonValue* parseNumber(JsonParser* parser) {
- const char* start = parser->input + parser->position;
- char* end = nullptr;
- double number = strtod(start, &end);
- if (end == start) {
- fprintf(stderr, "JsonReader: invalid number\n");
- return nullptr;
- }
- parser->position += (size_t) (end - start);
-
- JsonValue* value = makeValue(JSON_NUMBER);
- value->numberValue = number;
- return value;
-}
-
-static JsonValue* parseArray(JsonParser* parser) {
- // Skip opening bracket
- advance(parser);
-
- JsonValue* value = makeValue(JSON_ARRAY);
- value->array.items = nullptr;
- value->array.count = 0;
- value->array.capacity = 0;
-
- skipWhitespace(parser);
- if (peek(parser) == ']') {
- advance(parser);
- return value;
- }
-
- while (true) {
- skipWhitespace(parser);
- JsonValue* item = parseValue(parser);
- if (item == nullptr) {
- JsonReader_free(value);
- return nullptr;
- }
-
- // Grow items array if needed
- if (value->array.count >= value->array.capacity) {
- int newCapacity = (value->array.capacity == 0) ? 8 : value->array.capacity * 2;
- value->array.items = safeRealloc(value->array.items, (size_t) newCapacity * sizeof(JsonValue));
- if (value->array.items == nullptr) {
- fprintf(stderr, "JsonReader: realloc failed\n");
- abort();
- }
- value->array.capacity = newCapacity;
- }
-
- // Copy item into array and free the container
- value->array.items[value->array.count++] = *item;
- free(item);
-
- skipWhitespace(parser);
- if (peek(parser) == ',') {
- advance(parser);
- } else if (peek(parser) == ']') {
- advance(parser);
- return value;
- } else {
- fprintf(stderr, "JsonReader: expected ',' or ']' in array\n");
- JsonReader_free(value);
- return nullptr;
- }
- }
-}
-
-static JsonValue* parseObject(JsonParser* parser) {
- // Skip opening brace
- advance(parser);
-
- JsonValue* value = makeValue(JSON_OBJECT);
- value->object.keys = nullptr;
- value->object.values = nullptr;
- value->object.count = 0;
- value->object.capacity = 0;
-
- skipWhitespace(parser);
- if (peek(parser) == '}') {
- advance(parser);
- return value;
- }
-
- while (true) {
- skipWhitespace(parser);
- if (peek(parser) != '"') {
- fprintf(stderr, "JsonReader: expected string key in object\n");
- JsonReader_free(value);
- return nullptr;
- }
-
- JsonValue* keyValue = parseString(parser);
- if (keyValue == nullptr) {
- JsonReader_free(value);
- return nullptr;
- }
- char* key = keyValue->stringValue;
- // Free just the JsonValue container, keep the string
- free(keyValue);
-
- skipWhitespace(parser);
- if (peek(parser) != ':') {
- fprintf(stderr, "JsonReader: expected ':' after object key\n");
- free(key);
- JsonReader_free(value);
- return nullptr;
- }
- advance(parser);
-
- skipWhitespace(parser);
- JsonValue* itemValue = parseValue(parser);
- if (itemValue == nullptr) {
- free(key);
- JsonReader_free(value);
- return nullptr;
- }
-
- // Grow arrays if needed
- if (value->object.count >= value->object.capacity) {
- int newCapacity = (value->object.capacity == 0) ? 8 : value->object.capacity * 2;
- value->object.keys = safeRealloc(value->object.keys, (size_t) newCapacity * sizeof(char*));
- value->object.values = safeRealloc(value->object.values, (size_t) newCapacity * sizeof(JsonValue));
- if (value->object.keys == nullptr || value->object.values == nullptr) {
- fprintf(stderr, "JsonReader: realloc failed\n");
- abort();
- }
- value->object.capacity = newCapacity;
- }
-
- value->object.keys[value->object.count] = key;
- value->object.values[value->object.count] = *itemValue;
- value->object.count++;
- free(itemValue);
-
- skipWhitespace(parser);
- if (peek(parser) == ',') {
- advance(parser);
- } else if (peek(parser) == '}') {
- advance(parser);
- return value;
- } else {
- fprintf(stderr, "JsonReader: expected ',' or '}' in object\n");
- JsonReader_free(value);
- return nullptr;
- }
- }
-}
-
-static JsonValue* parseLiteral(JsonParser* parser, const char* literal, size_t literalLen) {
- if (parser->position + literalLen > parser->length) {
- return nullptr;
- }
- if (memcmp(parser->input + parser->position, literal, literalLen) != 0) {
- return nullptr;
- }
- parser->position += literalLen;
- return makeValue(JSON_NULL); // Caller overrides type as needed
-}
-
-static JsonValue* parseValue(JsonParser* parser) {
- skipWhitespace(parser);
- char c = peek(parser);
-
- switch (c) {
- case '"':
- return parseString(parser);
- case '{':
- return parseObject(parser);
- case '[':
- return parseArray(parser);
- case 't': {
- JsonValue* value = parseLiteral(parser, "true", 4);
- if (value == nullptr) {
- fprintf(stderr, "JsonReader: invalid literal\n");
- return nullptr;
- }
- value->type = JSON_BOOL;
- value->boolValue = true;
- return value;
- }
- case 'f': {
- JsonValue* value = parseLiteral(parser, "false", 5);
- if (value == nullptr) {
- fprintf(stderr, "JsonReader: invalid literal\n");
- return nullptr;
- }
- value->type = JSON_BOOL;
- value->boolValue = false;
- return value;
- }
- case 'n': {
- JsonValue* value = parseLiteral(parser, "null", 4);
- if (value == nullptr) {
- fprintf(stderr, "JsonReader: invalid literal\n");
- return nullptr;
- }
- return value;
- }
- default:
- if (c == '-' || (c >= '0' && c <= '9')) {
- return parseNumber(parser);
- }
- fprintf(stderr, "JsonReader: unexpected character '%c' at position %zu\n", c, parser->position);
- return nullptr;
- }
-}
-
-// ===[ Lifecycle ]===
-
-JsonValue* JsonReader_parse(const char* json) {
- if (json == nullptr) return nullptr;
-
- JsonParser parser = {
- .input = json,
- .position = 0,
- .length = strlen(json),
- };
-
- JsonValue* result = parseValue(&parser);
-
- // Check for trailing non-whitespace
- if (result != nullptr) {
- skipWhitespace(&parser);
- if (parser.position < parser.length) {
- fprintf(stderr, "JsonReader: trailing content after JSON value at position %zu\n", parser.position);
- JsonReader_free(result);
- return nullptr;
- }
- }
-
- return result;
-}
-
-// Frees the contents of a JsonValue without freeing the JsonValue struct itself.
-// Used for inline values (array items, object values stored by value).
-static void freeContents(JsonValue* value) {
- switch (value->type) {
- case JSON_STRING:
- free(value->stringValue);
- break;
- case JSON_ARRAY:
- repeat(value->array.count, i) {
- freeContents(&value->array.items[i]);
- }
- free(value->array.items);
- break;
- case JSON_OBJECT:
- repeat(value->object.count, i) {
- free(value->object.keys[i]);
- freeContents(&value->object.values[i]);
- }
- free(value->object.keys);
- free(value->object.values);
- break;
- default:
- break;
- }
-}
-
-void JsonReader_free(JsonValue* value) {
- if (value == nullptr) return;
- freeContents(value);
- free(value);
-}
-
-// ===[ Type Checks ]===
-
-bool JsonReader_isNull(const JsonValue* value) {
- return value != nullptr && value->type == JSON_NULL;
-}
-
-bool JsonReader_isBool(const JsonValue* value) {
- return value != nullptr && value->type == JSON_BOOL;
-}
-
-bool JsonReader_isNumber(const JsonValue* value) {
- return value != nullptr && value->type == JSON_NUMBER;
-}
-
-bool JsonReader_isString(const JsonValue* value) {
- return value != nullptr && value->type == JSON_STRING;
-}
-
-bool JsonReader_isArray(const JsonValue* value) {
- return value != nullptr && value->type == JSON_ARRAY;
-}
-
-bool JsonReader_isObject(const JsonValue* value) {
- return value != nullptr && value->type == JSON_OBJECT;
-}
-
-// ===[ Value Getters ]===
-
-bool JsonReader_getBool(const JsonValue* value) {
- return value->boolValue;
-}
-
-double JsonReader_getDouble(const JsonValue* value) {
- return value->numberValue;
-}
-
-int64_t JsonReader_getInt(const JsonValue* value) {
- return (int64_t) value->numberValue;
-}
-
-const char* JsonReader_getString(const JsonValue* value) {
- return value->stringValue;
-}
-
-// ===[ Array Access ]===
-
-int JsonReader_arrayLength(const JsonValue* value) {
- return value->array.count;
-}
-
-JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index) {
- if (0 > index || index >= value->array.count) return nullptr;
- return &value->array.items[index];
-}
-
-// ===[ Array Bulk Read ]===
-
-void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen) {
- require(value != nullptr && value->type == JSON_ARRAY);
- require(value->array.count == expectedLen);
- repeat(expectedLen, i) {
- out[i] = (float) value->array.items[i].numberValue;
- }
-}
-
-void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen) {
- require(value != nullptr && value->type == JSON_ARRAY);
- require(value->array.count == expectedLen);
- repeat(expectedLen, i) {
- out[i] = (int32_t) value->array.items[i].numberValue;
- }
-}
-
-// ===[ Object Access ]===
-
-int JsonReader_objectLength(const JsonValue* value) {
- return value->object.count;
-}
-
-JsonValue* JsonReader_getObject(const JsonValue* value, const char* key) {
- repeat(value->object.count, i) {
- if (strcmp(value->object.keys[i], key) == 0) {
- return &value->object.values[i];
- }
- }
- return nullptr;
-}
-
-const char* JsonReader_getObjectKey(const JsonValue* value, int index) {
- if (0 > index || index >= value->object.count) return nullptr;
- return value->object.keys[index];
-}
-
-JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index) {
- if (0 > index || index >= value->object.count) return nullptr;
- return &value->object.values[index];
-}
+#include "json_reader.h"
+
+#include
+#include
+#include
+
+#include "utils.h"
+
+// ===[ Parser State ]===
+
+typedef struct {
+ const char* input;
+ size_t position;
+ size_t length;
+} JsonParser;
+
+// ===[ Internal Helpers ]===
+
+static void skipWhitespace(JsonParser* parser) {
+ while (parser->position < parser->length) {
+ char c = parser->input[parser->position];
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
+ parser->position++;
+ } else {
+ break;
+ }
+ }
+}
+
+static char peek(JsonParser* parser) {
+ if (parser->position >= parser->length) return '\0';
+ return parser->input[parser->position];
+}
+
+static char advance(JsonParser* parser) {
+ if (parser->position >= parser->length) return '\0';
+ return parser->input[parser->position++];
+}
+
+static JsonValue* makeValue(JsonValueType type) {
+ JsonValue* value = safeCalloc(1, sizeof(JsonValue));
+ if (value == nullptr) {
+ fprintf(stderr, "JsonReader: calloc failed\n");
+ abort();
+ }
+ value->type = type;
+ return value;
+}
+
+// Forward declaration for recursive parsing
+static JsonValue* parseValue(JsonParser* parser);
+
+static JsonValue* parseString(JsonParser* parser) {
+ // Skip opening quote
+ advance(parser);
+
+ size_t capacity = 64;
+ size_t length = 0;
+ char* buffer = safeMalloc(capacity);
+ if (buffer == nullptr) {
+ fprintf(stderr, "JsonReader: malloc failed\n");
+ abort();
+ }
+
+ while (parser->position < parser->length) {
+ char c = advance(parser);
+
+ if (c == '"') {
+ // End of string
+ buffer[length] = '\0';
+ JsonValue* value = makeValue(JSON_STRING);
+ value->stringValue = buffer;
+ return value;
+ }
+
+ if (c == '\\') {
+ // Escape sequence
+ char escaped = advance(parser);
+ switch (escaped) {
+ case '"': c = '"'; break;
+ case '\\': c = '\\'; break;
+ case '/': c = '/'; break;
+ case 'b': c = '\b'; break;
+ case 'f': c = '\f'; break;
+ case 'n': c = '\n'; break;
+ case 'r': c = '\r'; break;
+ case 't': c = '\t'; break;
+ case 'u': {
+ // Parse 4-digit hex code point
+ char hex[5] = {0};
+ repeat(4, i) {
+ hex[i] = advance(parser);
+ }
+ unsigned long codePoint = strtoul(hex, nullptr, 16);
+
+ // Encode as UTF-8
+ if (128 > codePoint) {
+ c = (char) codePoint;
+ } else if (2048 > codePoint) {
+ if (length + 2 >= capacity) {
+ capacity *= 2;
+ buffer = safeRealloc(buffer, capacity);
+ }
+ buffer[length++] = (char) (0xC0 | (codePoint >> 6));
+ c = (char) (0x80 | (codePoint & 0x3F));
+ } else {
+ if (length + 3 >= capacity) {
+ capacity *= 2;
+ buffer = safeRealloc(buffer, capacity);
+ }
+ buffer[length++] = (char) (0xE0 | (codePoint >> 12));
+ buffer[length++] = (char) (0x80 | ((codePoint >> 6) & 0x3F));
+ c = (char) (0x80 | (codePoint & 0x3F));
+ }
+ break;
+ }
+ default:
+ fprintf(stderr, "JsonReader: unknown escape sequence '\\%c'\n", escaped);
+ free(buffer);
+ return nullptr;
+ }
+ }
+
+ // Grow buffer if needed
+ if (length + 1 >= capacity) {
+ capacity *= 2;
+ buffer = safeRealloc(buffer, capacity);
+ if (buffer == nullptr) {
+ fprintf(stderr, "JsonReader: realloc failed\n");
+ abort();
+ }
+ }
+ buffer[length++] = c;
+ }
+
+ // Unterminated string
+ fprintf(stderr, "JsonReader: unterminated string\n");
+ free(buffer);
+ return nullptr;
+}
+
+static JsonValue* parseNumber(JsonParser* parser) {
+ const char* start = parser->input + parser->position;
+ char* end = nullptr;
+ double number = strtod(start, &end);
+ if (end == start) {
+ fprintf(stderr, "JsonReader: invalid number\n");
+ return nullptr;
+ }
+ parser->position += (size_t) (end - start);
+
+ JsonValue* value = makeValue(JSON_NUMBER);
+ value->numberValue = number;
+ return value;
+}
+
+static JsonValue* parseArray(JsonParser* parser) {
+ // Skip opening bracket
+ advance(parser);
+
+ JsonValue* value = makeValue(JSON_ARRAY);
+ value->array.items = nullptr;
+ value->array.count = 0;
+ value->array.capacity = 0;
+
+ skipWhitespace(parser);
+ if (peek(parser) == ']') {
+ advance(parser);
+ return value;
+ }
+
+ while (true) {
+ skipWhitespace(parser);
+ JsonValue* item = parseValue(parser);
+ if (item == nullptr) {
+ JsonReader_free(value);
+ return nullptr;
+ }
+
+ // Grow items array if needed
+ if (value->array.count >= value->array.capacity) {
+ int newCapacity = (value->array.capacity == 0) ? 8 : value->array.capacity * 2;
+ value->array.items = safeRealloc(value->array.items, (size_t) newCapacity * sizeof(JsonValue));
+ if (value->array.items == nullptr) {
+ fprintf(stderr, "JsonReader: realloc failed\n");
+ abort();
+ }
+ value->array.capacity = newCapacity;
+ }
+
+ // Copy item into array and free the container
+ value->array.items[value->array.count++] = *item;
+ free(item);
+
+ skipWhitespace(parser);
+ if (peek(parser) == ',') {
+ advance(parser);
+ } else if (peek(parser) == ']') {
+ advance(parser);
+ return value;
+ } else {
+ fprintf(stderr, "JsonReader: expected ',' or ']' in array\n");
+ JsonReader_free(value);
+ return nullptr;
+ }
+ }
+}
+
+static JsonValue* parseObject(JsonParser* parser) {
+ // Skip opening brace
+ advance(parser);
+
+ JsonValue* value = makeValue(JSON_OBJECT);
+ value->object.keys = nullptr;
+ value->object.values = nullptr;
+ value->object.count = 0;
+ value->object.capacity = 0;
+
+ skipWhitespace(parser);
+ if (peek(parser) == '}') {
+ advance(parser);
+ return value;
+ }
+
+ while (true) {
+ skipWhitespace(parser);
+ if (peek(parser) != '"') {
+ fprintf(stderr, "JsonReader: expected string key in object\n");
+ JsonReader_free(value);
+ return nullptr;
+ }
+
+ JsonValue* keyValue = parseString(parser);
+ if (keyValue == nullptr) {
+ JsonReader_free(value);
+ return nullptr;
+ }
+ char* key = keyValue->stringValue;
+ // Free just the JsonValue container, keep the string
+ free(keyValue);
+
+ skipWhitespace(parser);
+ if (peek(parser) != ':') {
+ fprintf(stderr, "JsonReader: expected ':' after object key\n");
+ free(key);
+ JsonReader_free(value);
+ return nullptr;
+ }
+ advance(parser);
+
+ skipWhitespace(parser);
+ JsonValue* itemValue = parseValue(parser);
+ if (itemValue == nullptr) {
+ free(key);
+ JsonReader_free(value);
+ return nullptr;
+ }
+
+ // Grow arrays if needed
+ if (value->object.count >= value->object.capacity) {
+ int newCapacity = (value->object.capacity == 0) ? 8 : value->object.capacity * 2;
+ value->object.keys = safeRealloc(value->object.keys, (size_t) newCapacity * sizeof(char*));
+ value->object.values = safeRealloc(value->object.values, (size_t) newCapacity * sizeof(JsonValue));
+ if (value->object.keys == nullptr || value->object.values == nullptr) {
+ fprintf(stderr, "JsonReader: realloc failed\n");
+ abort();
+ }
+ value->object.capacity = newCapacity;
+ }
+
+ value->object.keys[value->object.count] = key;
+ value->object.values[value->object.count] = *itemValue;
+ value->object.count++;
+ free(itemValue);
+
+ skipWhitespace(parser);
+ if (peek(parser) == ',') {
+ advance(parser);
+ } else if (peek(parser) == '}') {
+ advance(parser);
+ return value;
+ } else {
+ fprintf(stderr, "JsonReader: expected ',' or '}' in object\n");
+ JsonReader_free(value);
+ return nullptr;
+ }
+ }
+}
+
+static JsonValue* parseLiteral(JsonParser* parser, const char* literal, size_t literalLen) {
+ if (parser->position + literalLen > parser->length) {
+ return nullptr;
+ }
+ if (memcmp(parser->input + parser->position, literal, literalLen) != 0) {
+ return nullptr;
+ }
+ parser->position += literalLen;
+ return makeValue(JSON_NULL); // Caller overrides type as needed
+}
+
+static JsonValue* parseValue(JsonParser* parser) {
+ skipWhitespace(parser);
+ char c = peek(parser);
+
+ switch (c) {
+ case '"':
+ return parseString(parser);
+ case '{':
+ return parseObject(parser);
+ case '[':
+ return parseArray(parser);
+ case 't': {
+ JsonValue* value = parseLiteral(parser, "true", 4);
+ if (value == nullptr) {
+ fprintf(stderr, "JsonReader: invalid literal\n");
+ return nullptr;
+ }
+ value->type = JSON_BOOL;
+ value->boolValue = true;
+ return value;
+ }
+ case 'f': {
+ JsonValue* value = parseLiteral(parser, "false", 5);
+ if (value == nullptr) {
+ fprintf(stderr, "JsonReader: invalid literal\n");
+ return nullptr;
+ }
+ value->type = JSON_BOOL;
+ value->boolValue = false;
+ return value;
+ }
+ case 'n': {
+ JsonValue* value = parseLiteral(parser, "null", 4);
+ if (value == nullptr) {
+ fprintf(stderr, "JsonReader: invalid literal\n");
+ return nullptr;
+ }
+ return value;
+ }
+ default:
+ if (c == '-' || (c >= '0' && c <= '9')) {
+ return parseNumber(parser);
+ }
+ fprintf(stderr, "JsonReader: unexpected character '%c' at position %zu\n", c, parser->position);
+ return nullptr;
+ }
+}
+
+// ===[ Lifecycle ]===
+
+JsonValue* JsonReader_parse(const char* json) {
+ if (json == nullptr) return nullptr;
+
+ JsonParser parser = {
+ .input = json,
+ .position = 0,
+ .length = strlen(json),
+ };
+
+ JsonValue* result = parseValue(&parser);
+
+ // Check for trailing non-whitespace
+ if (result != nullptr) {
+ skipWhitespace(&parser);
+ if (parser.position < parser.length) {
+ fprintf(stderr, "JsonReader: trailing content after JSON value at position %zu\n", parser.position);
+ JsonReader_free(result);
+ return nullptr;
+ }
+ }
+
+ return result;
+}
+
+// Frees the contents of a JsonValue without freeing the JsonValue struct itself.
+// Used for inline values (array items, object values stored by value).
+static void freeContents(JsonValue* value) {
+ switch (value->type) {
+ case JSON_STRING:
+ free(value->stringValue);
+ break;
+ case JSON_ARRAY:
+ repeat(value->array.count, i) {
+ freeContents(&value->array.items[i]);
+ }
+ free(value->array.items);
+ break;
+ case JSON_OBJECT:
+ repeat(value->object.count, i) {
+ free(value->object.keys[i]);
+ freeContents(&value->object.values[i]);
+ }
+ free(value->object.keys);
+ free(value->object.values);
+ break;
+ default:
+ break;
+ }
+}
+
+void JsonReader_free(JsonValue* value) {
+ if (value == nullptr) return;
+ freeContents(value);
+ free(value);
+}
+
+// ===[ Type Checks ]===
+
+bool JsonReader_isNull(const JsonValue* value) {
+ return value != nullptr && value->type == JSON_NULL;
+}
+
+bool JsonReader_isBool(const JsonValue* value) {
+ return value != nullptr && value->type == JSON_BOOL;
+}
+
+bool JsonReader_isNumber(const JsonValue* value) {
+ return value != nullptr && value->type == JSON_NUMBER;
+}
+
+bool JsonReader_isString(const JsonValue* value) {
+ return value != nullptr && value->type == JSON_STRING;
+}
+
+bool JsonReader_isArray(const JsonValue* value) {
+ return value != nullptr && value->type == JSON_ARRAY;
+}
+
+bool JsonReader_isObject(const JsonValue* value) {
+ return value != nullptr && value->type == JSON_OBJECT;
+}
+
+// ===[ Value Getters ]===
+
+bool JsonReader_getBool(const JsonValue* value) {
+ return value->boolValue;
+}
+
+double JsonReader_getDouble(const JsonValue* value) {
+ return value->numberValue;
+}
+
+int64_t JsonReader_getInt(const JsonValue* value) {
+ return (int64_t) value->numberValue;
+}
+
+const char* JsonReader_getString(const JsonValue* value) {
+ return value->stringValue;
+}
+
+// ===[ Array Access ]===
+
+int JsonReader_arrayLength(const JsonValue* value) {
+ return value->array.count;
+}
+
+JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index) {
+ if (0 > index || index >= value->array.count) return nullptr;
+ return &value->array.items[index];
+}
+
+// ===[ Array Bulk Read ]===
+
+void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen) {
+ require(value != nullptr && value->type == JSON_ARRAY);
+ require(value->array.count == expectedLen);
+ repeat(expectedLen, i) {
+ out[i] = (float) value->array.items[i].numberValue;
+ }
+}
+
+void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen) {
+ require(value != nullptr && value->type == JSON_ARRAY);
+ require(value->array.count == expectedLen);
+ repeat(expectedLen, i) {
+ out[i] = (int32_t) value->array.items[i].numberValue;
+ }
+}
+
+// ===[ Object Access ]===
+
+int JsonReader_objectLength(const JsonValue* value) {
+ return value->object.count;
+}
+
+JsonValue* JsonReader_getObject(const JsonValue* value, const char* key) {
+ repeat(value->object.count, i) {
+ if (strcmp(value->object.keys[i], key) == 0) {
+ return &value->object.values[i];
+ }
+ }
+ return nullptr;
+}
+
+const char* JsonReader_getObjectKey(const JsonValue* value, int index) {
+ if (0 > index || index >= value->object.count) return nullptr;
+ return value->object.keys[index];
+}
+
+JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index) {
+ if (0 > index || index >= value->object.count) return nullptr;
+ return &value->object.values[index];
+}
diff --git a/src/json_reader.h b/src/json_reader.h
index bc5a58a2..84e11486 100644
--- a/src/json_reader.h
+++ b/src/json_reader.h
@@ -1,67 +1,67 @@
-#pragma once
-
-#include "common.h"
-#include
-#include
-
-// ===[ JsonValue Types ]===
-
-typedef enum {
- JSON_NULL,
- JSON_BOOL,
- JSON_NUMBER,
- JSON_STRING,
- JSON_ARRAY,
- JSON_OBJECT,
-} JsonValueType;
-
-typedef struct JsonValue {
- JsonValueType type;
- union {
- bool boolValue;
- double numberValue;
- char* stringValue;
- struct { struct JsonValue* items; int count; int capacity; } array;
- struct { char** keys; struct JsonValue* values; int count; int capacity; } object;
- };
-} JsonValue;
-
-// ===[ Lifecycle ]===
-
-JsonValue* JsonReader_parse(const char* json);
-void JsonReader_free(JsonValue* value);
-
-// ===[ Type Checks ]===
-
-bool JsonReader_isNull(const JsonValue* value);
-bool JsonReader_isBool(const JsonValue* value);
-bool JsonReader_isNumber(const JsonValue* value);
-bool JsonReader_isString(const JsonValue* value);
-bool JsonReader_isArray(const JsonValue* value);
-bool JsonReader_isObject(const JsonValue* value);
-
-// ===[ Value Getters ]===
-
-bool JsonReader_getBool(const JsonValue* value);
-double JsonReader_getDouble(const JsonValue* value);
-int64_t JsonReader_getInt(const JsonValue* value);
-const char* JsonReader_getString(const JsonValue* value);
-
-// ===[ Array Access ]===
-
-int JsonReader_arrayLength(const JsonValue* value);
-JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index);
-
-// ===[ Array Bulk Read ]===
-
-// Reads a JSON number array into a float C array. Asserts the array has exactly expectedLen elements.
-void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen);
-// Reads a JSON number array into an int32 C array. Asserts the array has exactly expectedLen elements.
-void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen);
-
-// ===[ Object Access ]===
-
-int JsonReader_objectLength(const JsonValue* value);
-JsonValue* JsonReader_getObject(const JsonValue* value, const char* key);
-const char* JsonReader_getObjectKey(const JsonValue* value, int index);
-JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index);
+#pragma once
+
+#include "common.h"
+#include
+#include
+
+// ===[ JsonValue Types ]===
+
+typedef enum {
+ JSON_NULL,
+ JSON_BOOL,
+ JSON_NUMBER,
+ JSON_STRING,
+ JSON_ARRAY,
+ JSON_OBJECT,
+} JsonValueType;
+
+typedef struct JsonValue {
+ JsonValueType type;
+ union {
+ bool boolValue;
+ double numberValue;
+ char* stringValue;
+ struct { struct JsonValue* items; int count; int capacity; } array;
+ struct { char** keys; struct JsonValue* values; int count; int capacity; } object;
+ };
+} JsonValue;
+
+// ===[ Lifecycle ]===
+
+JsonValue* JsonReader_parse(const char* json);
+void JsonReader_free(JsonValue* value);
+
+// ===[ Type Checks ]===
+
+bool JsonReader_isNull(const JsonValue* value);
+bool JsonReader_isBool(const JsonValue* value);
+bool JsonReader_isNumber(const JsonValue* value);
+bool JsonReader_isString(const JsonValue* value);
+bool JsonReader_isArray(const JsonValue* value);
+bool JsonReader_isObject(const JsonValue* value);
+
+// ===[ Value Getters ]===
+
+bool JsonReader_getBool(const JsonValue* value);
+double JsonReader_getDouble(const JsonValue* value);
+int64_t JsonReader_getInt(const JsonValue* value);
+const char* JsonReader_getString(const JsonValue* value);
+
+// ===[ Array Access ]===
+
+int JsonReader_arrayLength(const JsonValue* value);
+JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index);
+
+// ===[ Array Bulk Read ]===
+
+// Reads a JSON number array into a float C array. Asserts the array has exactly expectedLen elements.
+void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen);
+// Reads a JSON number array into an int32 C array. Asserts the array has exactly expectedLen elements.
+void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen);
+
+// ===[ Object Access ]===
+
+int JsonReader_objectLength(const JsonValue* value);
+JsonValue* JsonReader_getObject(const JsonValue* value, const char* key);
+const char* JsonReader_getObjectKey(const JsonValue* value, int index);
+JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index);
diff --git a/src/json_writer.c b/src/json_writer.c
index 9fbafa57..41a4b2f8 100644
--- a/src/json_writer.c
+++ b/src/json_writer.c
@@ -1,161 +1,161 @@
-#include "json_writer.h"
-#include "utils.h"
-
-#include
-#include
-#include
-
-// ===[ Internal Helpers ]===
-
-static void writeCommaIfNeeded(JsonWriter* writer) {
- if (writer->needsComma) {
- StringBuilder_appendChar(&writer->out, ',');
- }
-}
-
-static void writeEscapedString(JsonWriter* writer, const char* str) {
- StringBuilder_appendChar(&writer->out, '"');
- for (const char* p = str; *p != '\0'; p++) {
- unsigned char c = (unsigned char) *p;
- switch (c) {
- case '"': StringBuilder_append(&writer->out, "\\\""); break;
- case '\\': StringBuilder_append(&writer->out, "\\\\"); break;
- case '\b': StringBuilder_append(&writer->out, "\\b"); break;
- case '\f': StringBuilder_append(&writer->out, "\\f"); break;
- case '\n': StringBuilder_append(&writer->out, "\\n"); break;
- case '\r': StringBuilder_append(&writer->out, "\\r"); break;
- case '\t': StringBuilder_append(&writer->out, "\\t"); break;
- default:
- if (32 > c) {
- StringBuilder_appendFormat(&writer->out, "\\u%04x", c);
- } else {
- StringBuilder_appendChar(&writer->out, (char) c);
- }
- break;
- }
- }
- StringBuilder_appendChar(&writer->out, '"');
-}
-
-// ===[ Lifecycle ]===
-
-JsonWriter JsonWriter_create(void) {
- return (JsonWriter) {
- .out = StringBuilder_create(256),
- .needsComma = false,
- };
-}
-
-void JsonWriter_free(JsonWriter* writer) {
- StringBuilder_free(&writer->out);
-}
-
-// ===[ Structure ]===
-
-void JsonWriter_beginObject(JsonWriter* writer) {
- writeCommaIfNeeded(writer);
- StringBuilder_appendChar(&writer->out, '{');
- writer->needsComma = false;
-}
-
-void JsonWriter_endObject(JsonWriter* writer) {
- StringBuilder_appendChar(&writer->out, '}');
- writer->needsComma = true;
-}
-
-void JsonWriter_beginArray(JsonWriter* writer) {
- writeCommaIfNeeded(writer);
- StringBuilder_appendChar(&writer->out, '[');
- writer->needsComma = false;
-}
-
-void JsonWriter_endArray(JsonWriter* writer) {
- StringBuilder_appendChar(&writer->out, ']');
- writer->needsComma = true;
-}
-
-// ===[ Object Keys ]===
-
-void JsonWriter_key(JsonWriter* writer, const char* key) {
- writeCommaIfNeeded(writer);
- writeEscapedString(writer, key);
- StringBuilder_appendChar(&writer->out, ':');
- writer->needsComma = false;
-}
-
-// ===[ Values ]===
-
-void JsonWriter_string(JsonWriter* writer, const char* value) {
- writeCommaIfNeeded(writer);
- if (value == nullptr) {
- StringBuilder_append(&writer->out, "null");
- } else {
- writeEscapedString(writer, value);
- }
- writer->needsComma = true;
-}
-
-void JsonWriter_int(JsonWriter* writer, int64_t value) {
- writeCommaIfNeeded(writer);
- StringBuilder_appendFormat(&writer->out, "%lld", (long long) value);
- writer->needsComma = true;
-}
-
-void JsonWriter_double(JsonWriter* writer, double value) {
- writeCommaIfNeeded(writer);
- StringBuilder_appendFormat(&writer->out, "%.17g", value);
- writer->needsComma = true;
-}
-
-void JsonWriter_bool(JsonWriter* writer, bool value) {
- writeCommaIfNeeded(writer);
- StringBuilder_append(&writer->out, value ? "true" : "false");
- writer->needsComma = true;
-}
-
-void JsonWriter_null(JsonWriter* writer) {
- writeCommaIfNeeded(writer);
- StringBuilder_append(&writer->out, "null");
- writer->needsComma = true;
-}
-
-// ===[ Property Convenience ]===
-
-void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value) {
- JsonWriter_key(writer, key);
- JsonWriter_string(writer, value);
-}
-
-void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value) {
- JsonWriter_key(writer, key);
- JsonWriter_int(writer, value);
-}
-
-void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value) {
- JsonWriter_key(writer, key);
- JsonWriter_double(writer, value);
-}
-
-void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value) {
- JsonWriter_key(writer, key);
- JsonWriter_bool(writer, value);
-}
-
-void JsonWriter_propertyNull(JsonWriter* writer, const char* key) {
- JsonWriter_key(writer, key);
- JsonWriter_null(writer);
-}
-
-// ===[ Output ]===
-
-const char* JsonWriter_getOutput(const JsonWriter* writer) {
- return StringBuilder_data(&writer->out);
-}
-
-char* JsonWriter_copyOutput(const JsonWriter* writer) {
- return safeStrdup(StringBuilder_data(&writer->out));
-}
-
-size_t JsonWriter_getLength(const JsonWriter* writer) {
- return StringBuilder_length(&writer->out);
-}
+#include "json_writer.h"
+#include "utils.h"
+
+#include
+#include
+#include
+
+// ===[ Internal Helpers ]===
+
+static void writeCommaIfNeeded(JsonWriter* writer) {
+ if (writer->needsComma) {
+ StringBuilder_appendChar(&writer->out, ',');
+ }
+}
+
+static void writeEscapedString(JsonWriter* writer, const char* str) {
+ StringBuilder_appendChar(&writer->out, '"');
+ for (const char* p = str; *p != '\0'; p++) {
+ unsigned char c = (unsigned char) *p;
+ switch (c) {
+ case '"': StringBuilder_append(&writer->out, "\\\""); break;
+ case '\\': StringBuilder_append(&writer->out, "\\\\"); break;
+ case '\b': StringBuilder_append(&writer->out, "\\b"); break;
+ case '\f': StringBuilder_append(&writer->out, "\\f"); break;
+ case '\n': StringBuilder_append(&writer->out, "\\n"); break;
+ case '\r': StringBuilder_append(&writer->out, "\\r"); break;
+ case '\t': StringBuilder_append(&writer->out, "\\t"); break;
+ default:
+ if (32 > c) {
+ StringBuilder_appendFormat(&writer->out, "\\u%04x", c);
+ } else {
+ StringBuilder_appendChar(&writer->out, (char) c);
+ }
+ break;
+ }
+ }
+ StringBuilder_appendChar(&writer->out, '"');
+}
+
+// ===[ Lifecycle ]===
+
+JsonWriter JsonWriter_create(void) {
+ return (JsonWriter) {
+ .out = StringBuilder_create(256),
+ .needsComma = false,
+ };
+}
+
+void JsonWriter_free(JsonWriter* writer) {
+ StringBuilder_free(&writer->out);
+}
+
+// ===[ Structure ]===
+
+void JsonWriter_beginObject(JsonWriter* writer) {
+ writeCommaIfNeeded(writer);
+ StringBuilder_appendChar(&writer->out, '{');
+ writer->needsComma = false;
+}
+
+void JsonWriter_endObject(JsonWriter* writer) {
+ StringBuilder_appendChar(&writer->out, '}');
+ writer->needsComma = true;
+}
+
+void JsonWriter_beginArray(JsonWriter* writer) {
+ writeCommaIfNeeded(writer);
+ StringBuilder_appendChar(&writer->out, '[');
+ writer->needsComma = false;
+}
+
+void JsonWriter_endArray(JsonWriter* writer) {
+ StringBuilder_appendChar(&writer->out, ']');
+ writer->needsComma = true;
+}
+
+// ===[ Object Keys ]===
+
+void JsonWriter_key(JsonWriter* writer, const char* key) {
+ writeCommaIfNeeded(writer);
+ writeEscapedString(writer, key);
+ StringBuilder_appendChar(&writer->out, ':');
+ writer->needsComma = false;
+}
+
+// ===[ Values ]===
+
+void JsonWriter_string(JsonWriter* writer, const char* value) {
+ writeCommaIfNeeded(writer);
+ if (value == nullptr) {
+ StringBuilder_append(&writer->out, "null");
+ } else {
+ writeEscapedString(writer, value);
+ }
+ writer->needsComma = true;
+}
+
+void JsonWriter_int(JsonWriter* writer, int64_t value) {
+ writeCommaIfNeeded(writer);
+ StringBuilder_appendFormat(&writer->out, "%lld", (long long) value);
+ writer->needsComma = true;
+}
+
+void JsonWriter_double(JsonWriter* writer, double value) {
+ writeCommaIfNeeded(writer);
+ StringBuilder_appendFormat(&writer->out, "%.17g", value);
+ writer->needsComma = true;
+}
+
+void JsonWriter_bool(JsonWriter* writer, bool value) {
+ writeCommaIfNeeded(writer);
+ StringBuilder_append(&writer->out, value ? "true" : "false");
+ writer->needsComma = true;
+}
+
+void JsonWriter_null(JsonWriter* writer) {
+ writeCommaIfNeeded(writer);
+ StringBuilder_append(&writer->out, "null");
+ writer->needsComma = true;
+}
+
+// ===[ Property Convenience ]===
+
+void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value) {
+ JsonWriter_key(writer, key);
+ JsonWriter_string(writer, value);
+}
+
+void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value) {
+ JsonWriter_key(writer, key);
+ JsonWriter_int(writer, value);
+}
+
+void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value) {
+ JsonWriter_key(writer, key);
+ JsonWriter_double(writer, value);
+}
+
+void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value) {
+ JsonWriter_key(writer, key);
+ JsonWriter_bool(writer, value);
+}
+
+void JsonWriter_propertyNull(JsonWriter* writer, const char* key) {
+ JsonWriter_key(writer, key);
+ JsonWriter_null(writer);
+}
+
+// ===[ Output ]===
+
+const char* JsonWriter_getOutput(const JsonWriter* writer) {
+ return StringBuilder_data(&writer->out);
+}
+
+char* JsonWriter_copyOutput(const JsonWriter* writer) {
+ return safeStrdup(StringBuilder_data(&writer->out));
+}
+
+size_t JsonWriter_getLength(const JsonWriter* writer) {
+ return StringBuilder_length(&writer->out);
+}
diff --git a/src/json_writer.h b/src/json_writer.h
index 905ec26e..6b8ee7cb 100644
--- a/src/json_writer.h
+++ b/src/json_writer.h
@@ -1,51 +1,51 @@
-#pragma once
-
-#include "common.h"
-#include "string_builder.h"
-#include
-#include
-
-// ===[ JsonWriter Type ]===
-
-typedef struct {
- StringBuilder out;
- bool needsComma;
-} JsonWriter;
-
-// ===[ Lifecycle ]===
-
-JsonWriter JsonWriter_create(void);
-void JsonWriter_free(JsonWriter* writer);
-
-// ===[ Structure ]===
-
-void JsonWriter_beginObject(JsonWriter* writer);
-void JsonWriter_endObject(JsonWriter* writer);
-void JsonWriter_beginArray(JsonWriter* writer);
-void JsonWriter_endArray(JsonWriter* writer);
-
-// ===[ Object Keys ]===
-
-void JsonWriter_key(JsonWriter* writer, const char* key);
-
-// ===[ Values ]===
-
-void JsonWriter_string(JsonWriter* writer, const char* value);
-void JsonWriter_int(JsonWriter* writer, int64_t value);
-void JsonWriter_double(JsonWriter* writer, double value);
-void JsonWriter_bool(JsonWriter* writer, bool value);
-void JsonWriter_null(JsonWriter* writer);
-
-// ===[ Property Convenience ]===
-
-void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value);
-void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value);
-void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value);
-void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value);
-void JsonWriter_propertyNull(JsonWriter* writer, const char* key);
-
-// ===[ Output ]===
-
-const char* JsonWriter_getOutput(const JsonWriter* writer);
-char* JsonWriter_copyOutput(const JsonWriter* writer);
-size_t JsonWriter_getLength(const JsonWriter* writer);
+#pragma once
+
+#include "common.h"
+#include "string_builder.h"
+#include
+#include
+
+// ===[ JsonWriter Type ]===
+
+typedef struct {
+ StringBuilder out;
+ bool needsComma;
+} JsonWriter;
+
+// ===[ Lifecycle ]===
+
+JsonWriter JsonWriter_create(void);
+void JsonWriter_free(JsonWriter* writer);
+
+// ===[ Structure ]===
+
+void JsonWriter_beginObject(JsonWriter* writer);
+void JsonWriter_endObject(JsonWriter* writer);
+void JsonWriter_beginArray(JsonWriter* writer);
+void JsonWriter_endArray(JsonWriter* writer);
+
+// ===[ Object Keys ]===
+
+void JsonWriter_key(JsonWriter* writer, const char* key);
+
+// ===[ Values ]===
+
+void JsonWriter_string(JsonWriter* writer, const char* value);
+void JsonWriter_int(JsonWriter* writer, int64_t value);
+void JsonWriter_double(JsonWriter* writer, double value);
+void JsonWriter_bool(JsonWriter* writer, bool value);
+void JsonWriter_null(JsonWriter* writer);
+
+// ===[ Property Convenience ]===
+
+void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value);
+void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value);
+void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value);
+void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value);
+void JsonWriter_propertyNull(JsonWriter* writer, const char* key);
+
+// ===[ Output ]===
+
+const char* JsonWriter_getOutput(const JsonWriter* writer);
+char* JsonWriter_copyOutput(const JsonWriter* writer);
+size_t JsonWriter_getLength(const JsonWriter* writer);
diff --git a/src/matrix_math.h b/src/matrix_math.h
index defff8d4..5f893fad 100644
--- a/src/matrix_math.h
+++ b/src/matrix_math.h
@@ -1,152 +1,152 @@
-#pragma once
-
-#include "common.h"
-#include
-#include
-
-// ===[ Matrix4f Type ]===
-
-// Column-major 4x4 matrix (OpenGL native layout)
-// Layout:
-// m[0] m[4] m[8] m[12] (tx)
-// m[1] m[5] m[9] m[13] (ty)
-// m[2] m[6] m[10] m[14] (tz)
-// m[3] m[7] m[11] m[15] (1)
-typedef struct {
- float m[16]; // m[col*4 + row]
-} Matrix4f;
-
-// ===[ Identity / Copy ]===
-
-static Matrix4f* Matrix4f_identity(Matrix4f* dest) {
- memset(dest->m, 0, sizeof(dest->m));
- dest->m[0] = 1.0f;
- dest->m[5] = 1.0f;
- dest->m[10] = 1.0f;
- dest->m[15] = 1.0f;
- return dest;
-}
-
-static Matrix4f* Matrix4f_copy(Matrix4f* dest, const Matrix4f* src) {
- memcpy(dest->m, src->m, sizeof(dest->m));
- return dest;
-}
-
-// ===[ Multiply ]===
-
-// dest = a * b (safe if dest aliases a or b)
-static Matrix4f* Matrix4f_multiply(Matrix4f* dest, const Matrix4f* a, const Matrix4f* b) {
- float tmp[16];
- for (int col = 0; 4 > col; col++) {
- for (int row = 0; 4 > row; row++) {
- tmp[col * 4 + row] =
- a->m[0 * 4 + row] * b->m[col * 4 + 0] +
- a->m[1 * 4 + row] * b->m[col * 4 + 1] +
- a->m[2 * 4 + row] * b->m[col * 4 + 2] +
- a->m[3 * 4 + row] * b->m[col * 4 + 3];
- }
- }
- memcpy(dest->m, tmp, sizeof(tmp));
- return dest;
-}
-
-// ===[ Orthographic Projection ]===
-
-// Post-multiply orthographic projection onto dest: dest = dest * ortho(l, r, b, t, n, f)
-static Matrix4f* Matrix4f_ortho(Matrix4f* dest, float left, float right, float bottom, float top, float near, float far) {
- Matrix4f ortho;
- memset(ortho.m, 0, sizeof(ortho.m));
- ortho.m[0] = 2.0f / (right - left);
- ortho.m[5] = 2.0f / (top - bottom);
- ortho.m[10] = -2.0f / (far - near);
- ortho.m[12] = -(right + left) / (right - left);
- ortho.m[13] = -(top + bottom) / (top - bottom);
- ortho.m[14] = -(far + near) / (far - near);
- ortho.m[15] = 1.0f;
- return Matrix4f_multiply(dest, dest, &ortho);
-}
-
-// ===[ Translate ]===
-
-// Post-multiply translation onto dest: dest = dest * T(x, y, z)
-// Optimized: only column 3 changes when post-multiplying a translation matrix
-static Matrix4f* Matrix4f_translate(Matrix4f* dest, float x, float y, float z) {
- dest->m[12] += dest->m[0] * x + dest->m[4] * y + dest->m[8] * z;
- dest->m[13] += dest->m[1] * x + dest->m[5] * y + dest->m[9] * z;
- dest->m[14] += dest->m[2] * x + dest->m[6] * y + dest->m[10] * z;
- dest->m[15] += dest->m[3] * x + dest->m[7] * y + dest->m[11] * z;
- return dest;
-}
-
-// ===[ Rotate Z ]===
-
-// Post-multiply Z-axis rotation onto dest: dest = dest * Rz(angleRadians)
-static Matrix4f* Matrix4f_rotateZ(Matrix4f* dest, float angleRadians) {
- float c = cosf(angleRadians);
- float s = sinf(angleRadians);
- // Columns 0 and 1 are affected: new_col0 = col0*c + col1*s, new_col1 = col0*(-s) + col1*c
- for (int row = 0; 4 > row; row++) {
- float a0 = dest->m[0 * 4 + row];
- float a1 = dest->m[1 * 4 + row];
- dest->m[0 * 4 + row] = a0 * c + a1 * s;
- dest->m[1 * 4 + row] = a0 * (-s) + a1 * c;
- }
- return dest;
-}
-
-// ===[ Scale ]===
-
-// Post-multiply scale onto dest: dest = dest * S(sx, sy, sz)
-// Optimized: scales each column directly
-static Matrix4f* Matrix4f_scale(Matrix4f* dest, float sx, float sy, float sz) {
- for (int row = 0; 4 > row; row++) {
- dest->m[0 * 4 + row] *= sx;
- dest->m[1 * 4 + row] *= sy;
- dest->m[2 * 4 + row] *= sz;
- }
- return dest;
-}
-
-// ===[ Set Transform 2D ]===
-
-// Directly sets dest to a combined translate * rotateZ * scale matrix (no post-multiply)
-// Equivalent to: identity -> translate(x, y, 0) -> rotateZ(angleRad) -> scale(sx, sy, 1)
-static Matrix4f* Matrix4f_setTransform2D(Matrix4f* dest, float x, float y, float sx, float sy, float angleRad) {
- float c = cosf(angleRad);
- float s = sinf(angleRad);
-
- // Column 0: rotated+scaled X axis
- dest->m[0] = c * sx;
- dest->m[1] = s * sx;
- dest->m[2] = 0.0f;
- dest->m[3] = 0.0f;
-
- // Column 1: rotated+scaled Y axis
- dest->m[4] = -s * sy;
- dest->m[5] = c * sy;
- dest->m[6] = 0.0f;
- dest->m[7] = 0.0f;
-
- // Column 2: Z axis (identity for 2D)
- dest->m[8] = 0.0f;
- dest->m[9] = 0.0f;
- dest->m[10] = 1.0f;
- dest->m[11] = 0.0f;
-
- // Column 3: translation
- dest->m[12] = x;
- dest->m[13] = y;
- dest->m[14] = 0.0f;
- dest->m[15] = 1.0f;
-
- return dest;
-}
-
-// ===[ Transform Point ]===
-
-// Transform a 2D point (x, y) through the matrix (w=1), writing results to outX, outY
-// Useful for CPU-side vertex transforms (e.g. PS2/gsKit software rendering)
-static void Matrix4f_transformPoint(const Matrix4f* mat, float x, float y, float* outX, float* outY) {
- *outX = mat->m[0] * x + mat->m[4] * y + mat->m[12];
- *outY = mat->m[1] * x + mat->m[5] * y + mat->m[13];
-}
+#pragma once
+
+#include "common.h"
+#include
+#include
+
+// ===[ Matrix4f Type ]===
+
+// Column-major 4x4 matrix (OpenGL native layout)
+// Layout:
+// m[0] m[4] m[8] m[12] (tx)
+// m[1] m[5] m[9] m[13] (ty)
+// m[2] m[6] m[10] m[14] (tz)
+// m[3] m[7] m[11] m[15] (1)
+typedef struct {
+ float m[16]; // m[col*4 + row]
+} Matrix4f;
+
+// ===[ Identity / Copy ]===
+
+static Matrix4f* Matrix4f_identity(Matrix4f* dest) {
+ memset(dest->m, 0, sizeof(dest->m));
+ dest->m[0] = 1.0f;
+ dest->m[5] = 1.0f;
+ dest->m[10] = 1.0f;
+ dest->m[15] = 1.0f;
+ return dest;
+}
+
+static Matrix4f* Matrix4f_copy(Matrix4f* dest, const Matrix4f* src) {
+ memcpy(dest->m, src->m, sizeof(dest->m));
+ return dest;
+}
+
+// ===[ Multiply ]===
+
+// dest = a * b (safe if dest aliases a or b)
+static Matrix4f* Matrix4f_multiply(Matrix4f* dest, const Matrix4f* a, const Matrix4f* b) {
+ float tmp[16];
+ for (int col = 0; 4 > col; col++) {
+ for (int row = 0; 4 > row; row++) {
+ tmp[col * 4 + row] =
+ a->m[0 * 4 + row] * b->m[col * 4 + 0] +
+ a->m[1 * 4 + row] * b->m[col * 4 + 1] +
+ a->m[2 * 4 + row] * b->m[col * 4 + 2] +
+ a->m[3 * 4 + row] * b->m[col * 4 + 3];
+ }
+ }
+ memcpy(dest->m, tmp, sizeof(tmp));
+ return dest;
+}
+
+// ===[ Orthographic Projection ]===
+
+// Post-multiply orthographic projection onto dest: dest = dest * ortho(l, r, b, t, n, f)
+static Matrix4f* Matrix4f_ortho(Matrix4f* dest, float left, float right, float bottom, float top, float near, float far) {
+ Matrix4f ortho;
+ memset(ortho.m, 0, sizeof(ortho.m));
+ ortho.m[0] = 2.0f / (right - left);
+ ortho.m[5] = 2.0f / (top - bottom);
+ ortho.m[10] = -2.0f / (far - near);
+ ortho.m[12] = -(right + left) / (right - left);
+ ortho.m[13] = -(top + bottom) / (top - bottom);
+ ortho.m[14] = -(far + near) / (far - near);
+ ortho.m[15] = 1.0f;
+ return Matrix4f_multiply(dest, dest, &ortho);
+}
+
+// ===[ Translate ]===
+
+// Post-multiply translation onto dest: dest = dest * T(x, y, z)
+// Optimized: only column 3 changes when post-multiplying a translation matrix
+static Matrix4f* Matrix4f_translate(Matrix4f* dest, float x, float y, float z) {
+ dest->m[12] += dest->m[0] * x + dest->m[4] * y + dest->m[8] * z;
+ dest->m[13] += dest->m[1] * x + dest->m[5] * y + dest->m[9] * z;
+ dest->m[14] += dest->m[2] * x + dest->m[6] * y + dest->m[10] * z;
+ dest->m[15] += dest->m[3] * x + dest->m[7] * y + dest->m[11] * z;
+ return dest;
+}
+
+// ===[ Rotate Z ]===
+
+// Post-multiply Z-axis rotation onto dest: dest = dest * Rz(angleRadians)
+static Matrix4f* Matrix4f_rotateZ(Matrix4f* dest, float angleRadians) {
+ float c = cosf(angleRadians);
+ float s = sinf(angleRadians);
+ // Columns 0 and 1 are affected: new_col0 = col0*c + col1*s, new_col1 = col0*(-s) + col1*c
+ for (int row = 0; 4 > row; row++) {
+ float a0 = dest->m[0 * 4 + row];
+ float a1 = dest->m[1 * 4 + row];
+ dest->m[0 * 4 + row] = a0 * c + a1 * s;
+ dest->m[1 * 4 + row] = a0 * (-s) + a1 * c;
+ }
+ return dest;
+}
+
+// ===[ Scale ]===
+
+// Post-multiply scale onto dest: dest = dest * S(sx, sy, sz)
+// Optimized: scales each column directly
+static Matrix4f* Matrix4f_scale(Matrix4f* dest, float sx, float sy, float sz) {
+ for (int row = 0; 4 > row; row++) {
+ dest->m[0 * 4 + row] *= sx;
+ dest->m[1 * 4 + row] *= sy;
+ dest->m[2 * 4 + row] *= sz;
+ }
+ return dest;
+}
+
+// ===[ Set Transform 2D ]===
+
+// Directly sets dest to a combined translate * rotateZ * scale matrix (no post-multiply)
+// Equivalent to: identity -> translate(x, y, 0) -> rotateZ(angleRad) -> scale(sx, sy, 1)
+static Matrix4f* Matrix4f_setTransform2D(Matrix4f* dest, float x, float y, float sx, float sy, float angleRad) {
+ float c = cosf(angleRad);
+ float s = sinf(angleRad);
+
+ // Column 0: rotated+scaled X axis
+ dest->m[0] = c * sx;
+ dest->m[1] = s * sx;
+ dest->m[2] = 0.0f;
+ dest->m[3] = 0.0f;
+
+ // Column 1: rotated+scaled Y axis
+ dest->m[4] = -s * sy;
+ dest->m[5] = c * sy;
+ dest->m[6] = 0.0f;
+ dest->m[7] = 0.0f;
+
+ // Column 2: Z axis (identity for 2D)
+ dest->m[8] = 0.0f;
+ dest->m[9] = 0.0f;
+ dest->m[10] = 1.0f;
+ dest->m[11] = 0.0f;
+
+ // Column 3: translation
+ dest->m[12] = x;
+ dest->m[13] = y;
+ dest->m[14] = 0.0f;
+ dest->m[15] = 1.0f;
+
+ return dest;
+}
+
+// ===[ Transform Point ]===
+
+// Transform a 2D point (x, y) through the matrix (w=1), writing results to outX, outY
+// Useful for CPU-side vertex transforms (e.g. PS2/gsKit software rendering)
+static void Matrix4f_transformPoint(const Matrix4f* mat, float x, float y, float* outX, float* outY) {
+ *outX = mat->m[0] * x + mat->m[4] * y + mat->m[12];
+ *outY = mat->m[1] * x + mat->m[5] * y + mat->m[13];
+}
diff --git a/src/n3ds/main.c b/src/n3ds/main.c
new file mode 100644
index 00000000..2fb74a70
--- /dev/null
+++ b/src/n3ds/main.c
@@ -0,0 +1,874 @@
+#include "../data_win.h"
+#include "../runner.h"
+#include "../runner_keyboard.h"
+#include "../utils.h"
+#include "../vm.h"
+
+#include "n3ds_audio_system.h"
+#include "n3ds_file_system.h"
+#include "n3ds_renderer.h"
+
+#include <3ds.h>
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define N3DS_LOADING_TEXT_SCALE 0.42f
+#define N3DS_BOOT_LOG_MAX_LINES 6
+#define N3DS_BOOT_LOG_LINE_CHARS 88
+#define N3DS_TOTAL_VRAM_BYTES (6u * 1024u * 1024u)
+#define N3DS_DEBUG_ASRIEL_ROOM 330
+
+void N3DS_tryTriggerAsrielLed(Runner* runner);
+
+typedef struct {
+ bool useCitro2D;
+ C3D_RenderTarget* target;
+ C2D_TextBuf textBuf;
+ PrintConsole console;
+ char statusLine[128];
+ int chunkIndex;
+ int totalChunks;
+} N3DSLoadingScreen;
+
+static char gN3DSBootLogLines[N3DS_BOOT_LOG_MAX_LINES][N3DS_BOOT_LOG_LINE_CHARS];
+static int gN3DSBootLogLineCount = 0;
+
+typedef struct {
+ C2D_TextBuf textBuf;
+ double displayedFps;
+ double displayedRenderMs;
+ double sampledRenderMs;
+ uint32_t sampledFrames;
+ u64 sampleStartMs;
+} N3DSDebugMonitor;
+
+static bool fileExists(const char* path) {
+ struct stat st;
+ return stat(path, &st) == 0;
+}
+
+static void N3DS_formatDebugSize(char* out, size_t outSize, uint32_t bytes) {
+ if (out == NULL || outSize == 0) return;
+
+ double kib = (double) bytes / 1024.0;
+ if (kib < 1024.0) {
+ snprintf(out, outSize, "%.0f KB", kib);
+ return;
+ }
+
+ snprintf(out, outSize, "%.2f MB", kib / 1024.0);
+}
+
+static void N3DSDebugMonitor_init(N3DSDebugMonitor* monitor) {
+ if (monitor == NULL) return;
+ memset(monitor, 0, sizeof(*monitor));
+ monitor->textBuf = C2D_TextBufNew(1024);
+ monitor->sampleStartMs = osGetTime();
+}
+
+static void N3DSDebugMonitor_free(N3DSDebugMonitor* monitor) {
+ if (monitor == NULL) return;
+ if (monitor->textBuf != NULL) {
+ C2D_TextBufDelete(monitor->textBuf);
+ monitor->textBuf = NULL;
+ }
+}
+
+static void N3DSDebugMonitor_tickFrame(N3DSDebugMonitor* monitor, double renderMs) {
+ if (monitor == NULL) return;
+
+ monitor->sampledFrames++;
+ monitor->sampledRenderMs += renderMs;
+ u64 nowMs = osGetTime();
+ u64 elapsedMs = nowMs - monitor->sampleStartMs;
+ if (elapsedMs < 250) return;
+
+ monitor->displayedFps = ((double) monitor->sampledFrames * 1000.0) / (double) elapsedMs;
+ monitor->displayedRenderMs = monitor->sampledFrames > 0
+ ? monitor->sampledRenderMs / (double) monitor->sampledFrames
+ : 0.0;
+ monitor->sampledFrames = 0;
+ monitor->sampledRenderMs = 0.0;
+ monitor->sampleStartMs = nowMs;
+}
+
+//font used in godmode9 that's easy to read. got lazy and just embedded the pbm bytes directly here
+
+static uint8_t N3DSDebugTinyFont_getRow(char c, uint32_t row) {
+ static const uint8_t font[95][10] = {
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00 },
+ { 0x00, 0x0A, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A, 0x00, 0x00 },
+ { 0x00, 0x04, 0x0E, 0x15, 0x0C, 0x06, 0x15, 0x0E, 0x04, 0x00 },
+ { 0x00, 0x19, 0x19, 0x02, 0x04, 0x08, 0x13, 0x13, 0x00, 0x00 },
+ { 0x00, 0x04, 0x0A, 0x04, 0x09, 0x15, 0x12, 0x0D, 0x00, 0x00 },
+ { 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x02, 0x04, 0x04, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00 },
+ { 0x00, 0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x00, 0x00 },
+ { 0x00, 0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x18, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00 },
+ { 0x00, 0x02, 0x02, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x10, 0x10, 0x1E, 0x01, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x01, 0x02, 0x02, 0x04, 0x04, 0x04, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x18, 0x00 },
+ { 0x00, 0x01, 0x02, 0x04, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x13, 0x15, 0x17, 0x10, 0x0F, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x1E, 0x09, 0x09, 0x0E, 0x09, 0x09, 0x1E, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x1E, 0x09, 0x09, 0x09, 0x09, 0x09, 0x1E, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10, 0x00, 0x00 },
+ { 0x00, 0x0F, 0x10, 0x10, 0x13, 0x11, 0x11, 0x0F, 0x00, 0x00 },
+ { 0x00, 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x18, 0x00, 0x00 },
+ { 0x00, 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00, 0x00 },
+ { 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x11, 0x1B, 0x15, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D, 0x00, 0x00 },
+ { 0x00, 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00 },
+ { 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00, 0x00 },
+ { 0x00, 0x11, 0x11, 0x11, 0x11, 0x15, 0x15, 0x0A, 0x00, 0x00 },
+ { 0x00, 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00 },
+ { 0x00, 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x00, 0x00 },
+ { 0x00, 0x10, 0x10, 0x08, 0x08, 0x04, 0x04, 0x02, 0x02, 0x00 },
+ { 0x00, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0C, 0x00, 0x00 },
+ { 0x00, 0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00 },
+ { 0x00, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F, 0x00, 0x00 },
+ { 0x00, 0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x1E, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0E, 0x11, 0x10, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x01, 0x01, 0x0F, 0x11, 0x11, 0x13, 0x0D, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0F, 0x00, 0x00 },
+ { 0x00, 0x06, 0x09, 0x08, 0x1E, 0x08, 0x08, 0x08, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0D, 0x13, 0x11, 0x11, 0x0F, 0x01, 0x1E },
+ { 0x00, 0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x18 },
+ { 0x00, 0x10, 0x10, 0x12, 0x14, 0x1C, 0x12, 0x11, 0x00, 0x00 },
+ { 0x00, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x1A, 0x15, 0x15, 0x15, 0x15, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x1E, 0x10, 0x10 },
+ { 0x00, 0x00, 0x00, 0x0F, 0x11, 0x11, 0x13, 0x0D, 0x01, 0x01 },
+ { 0x00, 0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E, 0x00, 0x00 },
+ { 0x00, 0x04, 0x04, 0x0E, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0F, 0x01, 0x1E },
+ { 0x00, 0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F, 0x00, 0x00 },
+ { 0x00, 0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02, 0x00, 0x00 },
+ { 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 },
+ { 0x00, 0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08, 0x00, 0x00 },
+ { 0x00, 0x00, 0x00, 0x08, 0x15, 0x02, 0x00, 0x00, 0x00, 0x00 },
+ };
+
+ if (row >= 10u || c < 32 || c > 126) return 0;
+ return font[(uint8_t) c - 32u][row];
+}
+
+static void N3DSDebugTinyFont_drawText(const char* text, float x, float y, uint32_t color) {
+ if (text == NULL) return;
+
+ const float pixel = 1.0f;
+ const float charAdvance = 6.0f;
+ for (const char* cursor = text; *cursor != '\0'; ++cursor) {
+ if (*cursor == ' ') {
+ x += charAdvance;
+ continue;
+ }
+
+ repeat(10u, row) {
+ uint8_t bits = N3DSDebugTinyFont_getRow(*cursor, (uint32_t) row);
+ repeat(6u, col) {
+ if ((bits & (uint8_t) (1u << (5u - col))) == 0u) continue;
+ C2D_DrawRectSolid(x + (float) col * pixel, y + (float) row * pixel, 0.0f, pixel, pixel, color);
+ }
+ }
+ x += charAdvance;
+ }
+}
+
+static void N3DSDebugMonitor_draw(N3DSDebugMonitor* monitor, Runner* runner, Renderer* renderer) {
+#ifdef N3DS_DISABLE_BOTTOM_SCREEN
+ (void) monitor;
+ (void) runner;
+ (void) renderer;
+ return;
+#endif
+ if (monitor == NULL || runner == NULL || renderer == NULL || monitor->textBuf == NULL) return;
+
+ const char* roomName = "(none)";
+ if (runner->currentRoom != NULL && runner->currentRoom->name != NULL && runner->currentRoom->name[0] != '\0') {
+ roomName = runner->currentRoom->name;
+ }
+
+ uint32_t atlasVRAMBytes = N3DSRenderer_getResidentAtlasVRAMBytes(renderer);
+ uint32_t atlasVRAMLimitBytes = N3DSRenderer_getResidentAtlasVRAMLimitBytes(renderer);
+ uint32_t atlasPageCount = N3DSRenderer_getResidentAtlasPageCount(renderer);
+ uint32_t atlasPageLimit = N3DSRenderer_getResidentAtlasPageLimit(renderer);
+ uint32_t directVRAMBytes = N3DSRenderer_getResidentDirectAssetVRAMBytes(renderer);
+ uint32_t trackedVRAMBytes = atlasVRAMBytes + directVRAMBytes;
+ uint32_t ramFreeBytes = osGetMemRegionFree(MEMREGION_APPLICATION);
+ uint32_t ramTotalBytes = osGetMemRegionSize(MEMREGION_APPLICATION);
+ uint32_t linearFreeBytes = linearSpaceFree();
+
+ char fpsLine[96];
+ char vramLine[64];
+ char atlasLine[96];
+ char ramLine[96];
+ char audioLine[96];
+ char roomLine[96];
+ char vramUsed[24];
+ char vramTotal[24];
+ char atlasUsed[24];
+ char atlasLimit[24];
+ char directUsed[24];
+ char ramFree[24];
+ char ramTotal[24];
+ char linearFree[24];
+ char audioCached[24];
+ char audioLimit[24];
+ uint32_t cachedSounds = 0;
+ uint32_t totalSounds = 0;
+ uint32_t cachedSoundBytes = 0;
+ uint32_t cacheLimitBytes = 0;
+
+ N3DSAudioSystem_getCacheStats(runner->audioSystem, &cachedSounds, &totalSounds, &cachedSoundBytes, &cacheLimitBytes);
+
+ snprintf(
+ fpsLine,
+ sizeof(fpsLine),
+ "FPS %.1f R %.1fms",
+ monitor->displayedFps > 0.0 ? monitor->displayedFps : 0.0,
+ monitor->displayedRenderMs > 0.0 ? monitor->displayedRenderMs : 0.0
+ );
+ N3DS_formatDebugSize(vramUsed, sizeof(vramUsed), trackedVRAMBytes);
+ N3DS_formatDebugSize(vramTotal, sizeof(vramTotal), N3DS_TOTAL_VRAM_BYTES);
+ N3DS_formatDebugSize(atlasUsed, sizeof(atlasUsed), atlasVRAMBytes);
+ N3DS_formatDebugSize(atlasLimit, sizeof(atlasLimit), atlasVRAMLimitBytes);
+ N3DS_formatDebugSize(directUsed, sizeof(directUsed), directVRAMBytes);
+ N3DS_formatDebugSize(ramFree, sizeof(ramFree), ramFreeBytes);
+ N3DS_formatDebugSize(ramTotal, sizeof(ramTotal), ramTotalBytes);
+ N3DS_formatDebugSize(linearFree, sizeof(linearFree), linearFreeBytes);
+ N3DS_formatDebugSize(audioCached, sizeof(audioCached), cachedSoundBytes);
+ N3DS_formatDebugSize(audioLimit, sizeof(audioLimit), cacheLimitBytes);
+ snprintf(vramLine, sizeof(vramLine), "VRAM %s/%s",
+ vramUsed,
+ vramTotal);
+ snprintf(atlasLine, sizeof(atlasLine), "AT %lu/%lu %s DR %s",
+ (unsigned long) atlasPageCount,
+ (unsigned long) atlasPageLimit,
+ atlasUsed,
+ directUsed);
+ snprintf(ramLine, sizeof(ramLine), "RAM %s/%s L %s",
+ ramFree,
+ ramTotal,
+ linearFree);
+ snprintf(audioLine, sizeof(audioLine), "AUD %lu/%lu %s/%s",
+ (unsigned long) cachedSounds,
+ (unsigned long) totalSounds,
+ audioCached,
+ audioLimit);
+ snprintf(roomLine, sizeof(roomLine), "R %.28s", roomName);
+
+ N3DSRenderer_beginBottomScreenGUI(renderer, 320, 240);
+ const float boxX = 92.0f;
+ const float boxY = 8.0f;
+ const float boxW = 220.0f;
+ const float boxH = 110.0f;
+ const float textX = boxX + 7.0f;
+ C2D_DrawRectSolid(boxX, boxY, 0.0f, boxW, boxH, C2D_Color32(8, 10, 16, 218));
+ C2D_DrawRectSolid(boxX, boxY, 0.0f, boxW, 2.0f, C2D_Color32(77, 118, 255, 245));
+ C2D_DrawRectSolid(boxX, boxY + boxH - 1.0f, 0.0f, boxW, 1.0f, C2D_Color32(32, 46, 78, 230));
+ N3DSDebugTinyFont_drawText("DBG", textX, boxY + 7.0f, C2D_Color32(255, 255, 255, 255));
+ N3DSDebugTinyFont_drawText(fpsLine, boxX + 42.0f, boxY + 7.0f, C2D_Color32(196, 230, 255, 255));
+ N3DSDebugTinyFont_drawText(vramLine, textX, boxY + 25.0f, C2D_Color32(196, 230, 255, 255));
+ N3DSDebugTinyFont_drawText(atlasLine, textX, boxY + 42.0f, C2D_Color32(146, 188, 236, 255));
+ N3DSDebugTinyFont_drawText(ramLine, textX, boxY + 59.0f, C2D_Color32(196, 230, 255, 255));
+ N3DSDebugTinyFont_drawText(audioLine, textX, boxY + 76.0f, C2D_Color32(196, 255, 196, 255));
+ N3DSDebugTinyFont_drawText(roomLine, textX, boxY + 93.0f, C2D_Color32(255, 230, 163, 255));
+ N3DSRenderer_endBottomScreenGUI(renderer);
+}
+
+static bool N3DS_stringStartsWithIgnoreCase(const char* value, const char* prefix) {
+ if (value == NULL || prefix == NULL) return false;
+ while (*prefix != '\0') {
+ if (*value == '\0') return false;
+ if (tolower((unsigned char) *value) != tolower((unsigned char) *prefix)) return false;
+ ++value;
+ ++prefix;
+ }
+ return true;
+}
+
+static const char* N3DS_getRoomBorderAssetName(const Room* room) {
+ const char* roomName = room != NULL ? room->name : NULL;
+ if (roomName == NULL || roomName[0] == '\0') return "border_none";
+
+ if (N3DS_stringStartsWithIgnoreCase(roomName, "room_gaster") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_mysteryman")) return "room_gaster";
+
+ if (N3DS_stringStartsWithIgnoreCase(roomName, "room_truelab")) return "room_truelab";
+
+ if (N3DS_stringStartsWithIgnoreCase(roomName, "room_castle") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_asghouse") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_asgoreroom") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_lastruins_corridor") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_sanscorridor") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_undertale_end")) return "room_castle";
+
+ if (N3DS_stringStartsWithIgnoreCase(roomName, "room_fire")) return "room_fire";
+
+ if (N3DS_stringStartsWithIgnoreCase(roomName, "room_water")) return "room_water";
+
+ if (N3DS_stringStartsWithIgnoreCase(roomName, "room_tundra") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_ice") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_fogroom")) return "room_tundra";
+
+ if (N3DS_stringStartsWithIgnoreCase(roomName, "room_ruins") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_area1") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_torhouse") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_torielroom") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_asrielroom") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_kitchen") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_basement") ||
+ N3DS_stringStartsWithIgnoreCase(roomName, "room_ruinsexit")) return "room_ruins";
+
+ return "border_none";
+}
+
+static bool N3DS_tryLoadRoomBorderSprite(const char* assetName, C2D_Sprite* outSprite, C2D_SpriteSheet* outSheet) {
+ if (assetName == NULL || outSprite == NULL || outSheet == NULL) return false;
+
+ char candidatePaths[2][256];
+ snprintf(candidatePaths[0], sizeof(candidatePaths[0]), "sdmc:/3ds/cinnamon/gfx/borders/%s.t3x", assetName);
+ snprintf(candidatePaths[1], sizeof(candidatePaths[1]), "romfs:/gfx/borders/%s.t3x", assetName);
+
+ repeat(2, i) {
+ const char* path = candidatePaths[i];
+ C2D_SpriteSheet sheet = C2D_SpriteSheetLoad(path);
+ if (sheet == NULL) continue;
+ if (C2D_SpriteSheetCount(sheet) <= 0) {
+ C2D_SpriteSheetFree(sheet);
+ continue;
+ }
+
+ *outSheet = sheet;
+ C2D_SpriteFromSheet(outSprite, sheet, 0);
+ C2D_SpriteSetPos(outSprite, 0.0f, 0.0f);
+ C2D_SpriteSetDepth(outSprite, 1.0f);
+ return true;
+ }
+
+ return false;
+}
+
+static bool N3DS_refreshRoomBorderSprite(
+ const Room* room,
+ C2D_Sprite* borderSprite,
+ C2D_SpriteSheet* borderSheet,
+ bool* haveRoomBorder,
+ char* loadedBorderAssetName,
+ size_t loadedBorderAssetNameSize
+) {
+ if (borderSprite == NULL || borderSheet == NULL || haveRoomBorder == NULL ||
+ loadedBorderAssetName == NULL || loadedBorderAssetNameSize == 0) {
+ return false;
+ }
+
+ const char* desiredAssetName = N3DS_getRoomBorderAssetName(room);
+ if (strcmp(loadedBorderAssetName, desiredAssetName) == 0) return *haveRoomBorder;
+
+ if (*borderSheet != NULL) {
+ C2D_SpriteSheetFree(*borderSheet);
+ *borderSheet = NULL;
+ }
+
+ memset(borderSprite, 0, sizeof(*borderSprite));
+ *haveRoomBorder = N3DS_tryLoadRoomBorderSprite(desiredAssetName, borderSprite, borderSheet);
+ if (!*haveRoomBorder && strcmp(desiredAssetName, "border_none") != 0) {
+ *haveRoomBorder = N3DS_tryLoadRoomBorderSprite("border_none", borderSprite, borderSheet);
+ snprintf(loadedBorderAssetName, loadedBorderAssetNameSize, "%s", *haveRoomBorder ? "border_none" : desiredAssetName);
+ return *haveRoomBorder;
+ }
+
+ snprintf(loadedBorderAssetName, loadedBorderAssetNameSize, "%s", desiredAssetName);
+ return *haveRoomBorder;
+}
+
+static void N3DS_drawFallbackRoomBorder(void) {
+ const float screenW = 400.0f;
+ const float screenH = 240.0f;
+ const float outer = 6.0f;
+ const float inner = 3.0f;
+ const uint32_t outerColor = C2D_Color32(24, 18, 8, 255);
+ const uint32_t trimColor = C2D_Color32(208, 170, 84, 255);
+ const uint32_t highlightColor = C2D_Color32(255, 232, 170, 255);
+
+ const float overlayDepth = 1.0f;
+
+ C2D_DrawRectSolid(0.0f, 0.0f, overlayDepth, screenW, outer, outerColor);
+ C2D_DrawRectSolid(0.0f, screenH - outer, overlayDepth, screenW, outer, outerColor);
+ C2D_DrawRectSolid(0.0f, outer, overlayDepth, outer, screenH - outer * 2.0f, outerColor);
+ C2D_DrawRectSolid(screenW - outer, outer, overlayDepth, outer, screenH - outer * 2.0f, outerColor);
+
+ C2D_DrawRectSolid(outer, outer, overlayDepth, screenW - outer * 2.0f, inner, trimColor);
+ C2D_DrawRectSolid(outer, screenH - outer - inner, overlayDepth, screenW - outer * 2.0f, inner, trimColor);
+ C2D_DrawRectSolid(outer, outer + inner, overlayDepth, inner, screenH - (outer + inner) * 2.0f, trimColor);
+ C2D_DrawRectSolid(screenW - outer - inner, outer + inner, overlayDepth, inner, screenH - (outer + inner) * 2.0f, trimColor);
+
+ C2D_DrawRectSolid(outer + inner, outer + inner, overlayDepth, screenW - (outer + inner) * 2.0f, 1.0f, highlightColor);
+ C2D_DrawRectSolid(outer + inner, screenH - outer - inner - 1.0f, overlayDepth, screenW - (outer + inner) * 2.0f, 1.0f, highlightColor);
+}
+
+static char* chooseDataWinPath(void) {
+ if (fileExists("romfs:/data.win")) return safeStrdup("romfs:/data.win");
+ return safeStrdup("sdmc:/3ds/cinnamon/data.win");
+}
+
+static void syncKey(RunnerKeyboardState* keyboard, bool* state, int32_t key, bool held) {
+ if (held && !*state) RunnerKeyboard_onKeyDown(keyboard, key);
+ if (!held && *state) RunnerKeyboard_onKeyUp(keyboard, key);
+ *state = held;
+}
+
+static void N3DSLoadingScreen_free(N3DSLoadingScreen* screen) {
+ if (screen == NULL) return;
+ if (screen->textBuf != NULL) {
+ C2D_TextBufDelete(screen->textBuf);
+ screen->textBuf = NULL;
+ }
+ if (screen->target != NULL) {
+ C3D_RenderTargetDelete(screen->target);
+ screen->target = NULL;
+ }
+}
+
+static void N3DS_appendBootLog(const char* message) {
+ if (message == NULL || message[0] == '\0') return;
+
+ if (gN3DSBootLogLineCount < N3DS_BOOT_LOG_MAX_LINES) {
+ snprintf(
+ gN3DSBootLogLines[gN3DSBootLogLineCount],
+ sizeof(gN3DSBootLogLines[gN3DSBootLogLineCount]),
+ "%s",
+ message
+ );
+ gN3DSBootLogLineCount++;
+ return;
+ }
+
+ repeat(N3DS_BOOT_LOG_MAX_LINES - 1, i) {
+ snprintf(
+ gN3DSBootLogLines[i],
+ sizeof(gN3DSBootLogLines[i]),
+ "%s",
+ gN3DSBootLogLines[i + 1]
+ );
+ }
+ snprintf(
+ gN3DSBootLogLines[N3DS_BOOT_LOG_MAX_LINES - 1],
+ sizeof(gN3DSBootLogLines[N3DS_BOOT_LOG_MAX_LINES - 1]),
+ "%s",
+ message
+ );
+}
+
+void Runner_platformBootLog(const char* message) {
+ N3DS_appendBootLog(message);
+}
+
+static void N3DSLoadingScreen_draw(N3DSLoadingScreen* screen) {
+ if (screen == NULL) return;
+
+ float progress = 0.0f;
+ if (screen->totalChunks > 0) {
+ progress = (float) screen->chunkIndex / (float) screen->totalChunks;
+ if (progress < 0.0f) progress = 0.0f;
+ if (progress > 1.0f) progress = 1.0f;
+ }
+
+ if (screen->useCitro2D && screen->target != NULL && screen->textBuf != NULL) {
+ const float barX = 36.0f;
+ const float barY = 146.0f;
+ const float barW = 328.0f;
+ const float barH = 18.0f;
+ const float fillW = (barW - 4.0f) * progress;
+
+ C2D_Text title;
+ C2D_Text status;
+ C2D_Text detail;
+ char detailLine[64];
+ snprintf(detailLine, sizeof(detailLine), "%d / %d chunks", screen->chunkIndex, screen->totalChunks);
+
+ C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
+ C2D_TargetClear(screen->target, C2D_Color32(8, 10, 14, 255));
+ C2D_SceneBegin(screen->target);
+
+ C2D_DrawRectSolid(0.0f, 0.0f, 0.5f, 400.0f, 240.0f, C2D_Color32(8, 10, 14, 255));
+ C2D_DrawRectSolid(barX, barY, 0.5f, barW, barH, C2D_Color32(42, 48, 60, 255));
+ C2D_DrawRectSolid(barX + 2.0f, barY + 2.0f, 0.5f, fillW, barH - 4.0f, C2D_Color32(110, 224, 160, 255));
+
+ C2D_TextBufClear(screen->textBuf);
+ C2D_TextParse(&title, screen->textBuf, "Loading Game Data");
+ C2D_TextParse(&status, screen->textBuf, screen->statusLine);
+ C2D_TextParse(&detail, screen->textBuf, detailLine);
+ C2D_TextOptimize(&title);
+ C2D_TextOptimize(&status);
+ C2D_TextOptimize(&detail);
+
+ C2D_DrawText(&title, C2D_WithColor, 36.0f, 74.0f, 0.5f, 0.60f, 0.60f, C2D_Color32(255, 255, 255, 255));
+ C2D_DrawText(&status, C2D_WithColor, 36.0f, 104.0f, 0.5f, N3DS_LOADING_TEXT_SCALE, N3DS_LOADING_TEXT_SCALE, C2D_Color32(210, 214, 224, 255));
+ C2D_DrawText(&detail, C2D_WithColor, 36.0f, 172.0f, 0.5f, 0.34f, 0.34f, C2D_Color32(146, 153, 168, 255));
+
+ int firstLogLine = gN3DSBootLogLineCount > 3 ? gN3DSBootLogLineCount - 3 : 0;
+ float logY = 188.0f;
+ for (int i = firstLogLine; i < gN3DSBootLogLineCount; ++i) {
+ C2D_Text logLine;
+ C2D_TextParse(&logLine, screen->textBuf, gN3DSBootLogLines[i]);
+ C2D_TextOptimize(&logLine);
+ C2D_DrawText(&logLine, C2D_WithColor, 36.0f, logY, 0.5f, 0.25f, 0.25f, C2D_Color32(164, 176, 192, 255));
+ logY += 12.0f;
+ }
+
+ C3D_FrameEnd(0);
+ gspWaitForVBlank();
+ return;
+ }
+
+ int filled = (int) lroundf(progress * 24.0f);
+ if (filled < 0) filled = 0;
+ if (filled > 24) filled = 24;
+
+ char bar[25];
+ repeat(24, i) {
+ bar[i] = (int) i < filled ? '#' : '-';
+ }
+ bar[24] = '\0';
+
+ consoleSelect(&screen->console);
+ printf("\x1b[2J");
+ printf("\x1b[4;8HLoading Game Data");
+ printf("\x1b[7;8H%s", screen->statusLine);
+ printf("\x1b[10;8H[%s]", bar);
+ printf("\x1b[12;8H%d / %d chunks", screen->chunkIndex, screen->totalChunks);
+ printf("\x1b[15;8HPlease wait...");
+ int firstLogLine = gN3DSBootLogLineCount > 5 ? gN3DSBootLogLineCount - 5 : 0;
+ for (int i = firstLogLine; i < gN3DSBootLogLineCount; ++i) {
+ printf("\x1b[%d;4H%s", 17 + (i - firstLogLine), gN3DSBootLogLines[i]);
+ }
+
+ gfxFlushBuffers();
+ gfxSwapBuffers();
+ gspWaitForVBlank();
+}
+
+static void N3DSLoadingScreen_set(N3DSLoadingScreen* screen, const char* statusLine, int chunkIndex, int totalChunks) {
+ if (screen == NULL) return;
+ snprintf(screen->statusLine, sizeof(screen->statusLine), "%s", statusLine != NULL ? statusLine : "");
+ screen->chunkIndex = chunkIndex;
+ screen->totalChunks = totalChunks;
+ N3DSLoadingScreen_draw(screen);
+}
+
+static void N3DS_waitForStartExitScreen(N3DSLoadingScreen* screen, const char* statusLine) {
+ N3DSLoadingScreen_set(screen, statusLine, 1, 1);
+ while (aptMainLoop()) {
+ hidScanInput();
+ if (hidKeysDown() & KEY_START) break;
+ gspWaitForVBlank();
+ }
+}
+
+static s64 N3DS_ticksToNs(u64 ticks) {
+ return (s64) ((ticks * 1000000000ULL) / SYSCLOCK_ARM11);
+}
+
+static void N3DS_sleepUntilTick(u64 targetTick) {
+ const u64 coarseGuardTicks = SYSCLOCK_ARM11 / 2000u; // ~0.5 ms
+
+ while (true) {
+ u64 now = svcGetSystemTick();
+ if (now >= targetTick) return;
+
+ u64 remaining = targetTick - now;
+ if (remaining <= coarseGuardTicks) break;
+
+ svcSleepThread(N3DS_ticksToNs(remaining - coarseGuardTicks));
+ }
+
+ while (svcGetSystemTick() < targetTick) {
+ }
+}
+
+static void N3DS_beginPacedFrame(u64* nextFrameTick, u64 frameTicks) {
+ if (nextFrameTick == NULL || frameTicks == 0) return;
+
+ u64 now = svcGetSystemTick();
+ if (*nextFrameTick == 0) {
+ *nextFrameTick = now;
+ } else if (now > *nextFrameTick + frameTicks * 4u) {
+ *nextFrameTick = now;
+ } else {
+ while (now > *nextFrameTick) {
+ *nextFrameTick += frameTicks;
+ }
+ }
+
+ N3DS_sleepUntilTick(*nextFrameTick);
+ *nextFrameTick += frameTicks;
+}
+
+static void N3DSDataWinProgressCallback(const char* chunkName, int chunkIndex, int totalChunks, MAYBE_UNUSED DataWin* dataWin, void* userData) {
+ N3DSLoadingScreen* screen = (N3DSLoadingScreen*) userData;
+ if (screen == NULL) return;
+
+ char status[128];
+ snprintf(status, sizeof(status), "Parsing chunk %.4s", chunkName != NULL ? chunkName : "----");
+ N3DSLoadingScreen_set(screen, status, chunkIndex + 1, totalChunks);
+}
+
+int main(int argc, char** argv) {
+ (void) argc;
+ (void) argv;
+
+ gfxInitDefault();
+ romfsInit();
+ APT_SetAppCpuTimeLimit(30);
+ osSetSpeedupEnable(true);
+
+ bool citroReady = false;
+
+ if (C3D_Init(0x80000) && C2D_Init(C2D_DEFAULT_MAX_OBJECTS)) {
+ C2D_Prepare();
+ citroReady = true;
+ } else {
+ C2D_Fini();
+ C3D_Fini();
+ }
+
+ mkdir("sdmc:/3ds", 0777);
+ mkdir("sdmc:/3ds/cinnamon", 0777);
+
+ char* dataWinPath = chooseDataWinPath();
+
+ N3DSLoadingScreen loadingScreen = {0};
+ if (citroReady) {
+ loadingScreen.useCitro2D = true;
+ loadingScreen.target = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
+ loadingScreen.textBuf = C2D_TextBufNew(512);
+ if (loadingScreen.target == NULL || loadingScreen.textBuf == NULL) {
+ N3DSLoadingScreen_free(&loadingScreen);
+ loadingScreen.useCitro2D = false;
+ }
+ } else {
+ consoleInit(GFX_TOP, &loadingScreen.console);
+ }
+
+ N3DSLoadingScreen_set(&loadingScreen, "Scanning data.win", 0, 1);
+
+ DataWin* dataWin = DataWin_parse(
+ dataWinPath,
+ (DataWinParserOptions) {
+ .parseGen8 = true,
+ .parseOptn = true,
+ .parseLang = true,
+ .parseExtn = false,
+ .parseSond = true,
+ .parseAgrp = true,
+ .parseSprt = true,
+ .parseBgnd = true,
+ .parsePath = true,
+ .parseScpt = true,
+ .parseGlob = true,
+ .parseShdr = true,
+ .parseFont = true,
+ .parseTmln = true,
+ .parseObjt = true,
+ .parseRoom = true,
+ .parseTpag = true,
+ .parseCode = true,
+ .parseVari = true,
+ .parseFunc = true,
+ .parseStrg = true,
+ .parseTxtr = false,
+ .parseAudo = false,
+ .skipLoadingPreciseMasksForNonPreciseSprites = true,
+ .progressCallback = N3DSDataWinProgressCallback,
+ .progressCallbackUserData = &loadingScreen,
+ }
+ );
+
+ free(dataWinPath);
+
+ if (dataWin == NULL) {
+ N3DS_waitForStartExitScreen(&loadingScreen, "Failed to load data.win. Press START.");
+ N3DSLoadingScreen_free(&loadingScreen);
+ return 1;
+ }
+
+ FileSystem* fileSystem = (FileSystem*) N3DSFileSystem_create("romfs:/", "sdmc:/3ds/cinnamon/");
+ AudioSystem* audioSystem = (AudioSystem*) N3DSAudioSystem_create();
+ N3DSLoadingScreen_set(&loadingScreen, "Loading sound bank", 1, 2);
+ audioSystem->vtable->init(audioSystem, dataWin, fileSystem);
+
+ N3DSLoadingScreen_set(&loadingScreen, "Initializing renderer", 2, 2);
+ VMContext* vm = VM_create(dataWin);
+ Renderer* renderer = N3DSRenderer_create();
+ Runner* runner = Runner_create(dataWin, vm, renderer, fileSystem, audioSystem);
+
+ if (!N3DSRenderer_isReady(renderer)) {
+ const char* error = N3DSRenderer_getStartupError(renderer);
+ N3DS_waitForStartExitScreen(&loadingScreen, error != NULL ? error : "Renderer init failed. Press START.");
+ audioSystem->vtable->destroy(audioSystem);
+ renderer->vtable->destroy(renderer);
+ Runner_free(runner);
+ N3DSFileSystem_destroy((N3DSFileSystem*) fileSystem);
+ VM_free(vm);
+ DataWin_free(dataWin);
+ N3DSLoadingScreen_free(&loadingScreen);
+ if (citroReady) {
+ C2D_Fini();
+ C3D_Fini();
+ }
+ romfsExit();
+ gfxExit();
+ return 1;
+ }
+
+ N3DSLoadingScreen_free(&loadingScreen);
+ N3DSDebugMonitor debugMonitor;
+ N3DSDebugMonitor_init(&debugMonitor);
+
+ runner->osType = OS_3DS;
+ Runner_initFirstRoom(runner);
+
+ bool haveRoomBorder = false;
+ C2D_Sprite roomBorder;
+ memset(&roomBorder, 0, sizeof(roomBorder));
+ C2D_SpriteSheet roomBorderSheet = NULL;
+ char loadedRoomBorderAsset[32] = "";
+ bool debugMonitorVisible = false;
+ N3DS_refreshRoomBorderSprite(runner->currentRoom, &roomBorder, &roomBorderSheet, &haveRoomBorder, loadedRoomBorderAsset, sizeof(loadedRoomBorderAsset));
+
+ bool leftHeld = false, rightHeld = false, upHeld = false, downHeld = false;
+ bool aHeld = false, bHeld = false, yHeld = false;
+ bool lHeld = false, rHeld = false;
+
+ Gen8* gen8 = &dataWin->gen8;
+ int32_t gameW = (int32_t) gen8->defaultWindowWidth;
+ int32_t gameH = (int32_t) gen8->defaultWindowHeight;
+
+ const u64 frameTicks = (SYSCLOCK_ARM11 + 15u) / 30u;
+ u64 nextFrameTick = svcGetSystemTick();
+ while (aptMainLoop() && !runner->shouldExit) {
+ N3DS_beginPacedFrame(&nextFrameTick, frameTicks);
+ hidScanInput();
+ u32 held = hidKeysHeld();
+ u32 down = hidKeysDown();
+
+ if (down & KEY_START) break;
+ if (down & KEY_SELECT) debugMonitorVisible = !debugMonitorVisible;
+ bool debugWarpToAsriel = debugMonitorVisible && (down & KEY_L) &&
+ (uint32_t) N3DS_DEBUG_ASRIEL_ROOM < runner->dataWin->room.count;
+ if (debugWarpToAsriel) {
+ runner->pendingRoom = N3DS_DEBUG_ASRIEL_ROOM;
+ }
+
+ circlePosition circle;
+ hidCircleRead(&circle);
+
+ syncKey(runner->keyboard, &leftHeld, VK_LEFT, (held & KEY_LEFT) || circle.dx < -80);
+ syncKey(runner->keyboard, &rightHeld, VK_RIGHT, (held & KEY_RIGHT) || circle.dx > 80);
+ syncKey(runner->keyboard, &upHeld, VK_UP, (held & KEY_UP) || circle.dy > 80);
+ syncKey(runner->keyboard, &downHeld, VK_DOWN, (held & KEY_DOWN) || circle.dy < -80);
+
+ syncKey(runner->keyboard, &aHeld, 'Z', (held & KEY_A));
+ syncKey(runner->keyboard, &bHeld, 'X', (held & KEY_B));
+ syncKey(runner->keyboard, &yHeld, 'C', (held & KEY_Y) || (held & KEY_X));
+ syncKey(runner->keyboard, &lHeld, VK_PAGEDOWN, (held & KEY_L) && !debugWarpToAsriel);
+ syncKey(runner->keyboard, &rHeld, VK_PAGEUP, (held & KEY_R));
+
+ Runner_step(runner);
+ N3DS_tryTriggerAsrielLed(runner);
+ runner->audioSystem->vtable->update(runner->audioSystem, 1.0f / 30.0f);
+ N3DS_refreshRoomBorderSprite(runner->currentRoom, &roomBorder, &roomBorderSheet, &haveRoomBorder, loadedRoomBorderAsset, sizeof(loadedRoomBorderAsset));
+
+ float displayScaleX = 1.0f;
+ float displayScaleY = 1.0f;
+ Runner_computeViewDisplayScale(runner, gameW, gameH, &displayScaleX, &displayScaleY);
+ u64 renderStartMs = osGetTime();
+ C3D_FrameBegin(0);
+ renderer->vtable->beginFrame(renderer, gameW, gameH, 400, 240);
+ Runner_drawViews(runner, gameW, gameH, displayScaleX, displayScaleY, false);
+ N3DSRenderer_beginOverlay(renderer);
+ if (haveRoomBorder) C2D_DrawSprite(&roomBorder);
+ else N3DS_drawFallbackRoomBorder();
+ if (debugMonitorVisible) {
+ N3DSDebugMonitor_draw(&debugMonitor, runner, renderer);
+ }
+ renderer->vtable->endFrame(renderer);
+ C3D_FrameEnd(0);
+ N3DSDebugMonitor_tickFrame(&debugMonitor, (double) (osGetTime() - renderStartMs));
+
+ RunnerKeyboard_beginFrame(runner->keyboard);
+ }
+
+ audioSystem->vtable->destroy(audioSystem);
+ renderer->vtable->destroy(renderer);
+ Runner_free(runner);
+ N3DSFileSystem_destroy((N3DSFileSystem*) fileSystem);
+ VM_free(vm);
+ DataWin_free(dataWin);
+ N3DSDebugMonitor_free(&debugMonitor);
+
+ if (roomBorderSheet != NULL) {
+ C2D_SpriteSheetFree(roomBorderSheet);
+ }
+
+ if (citroReady) {
+ C2D_Fini();
+ C3D_Fini();
+ }
+
+ romfsExit();
+ gfxExit();
+ return 0;
+}
diff --git a/src/n3ds/n3ds_audio_system.c b/src/n3ds/n3ds_audio_system.c
new file mode 100644
index 00000000..8eed638a
--- /dev/null
+++ b/src/n3ds/n3ds_audio_system.c
@@ -0,0 +1,3830 @@
+#include "n3ds_audio_system.h"
+#include "n3ds_debug_log.h"
+#include "n3ds_platform_config.h"
+
+#include "../data_win.h"
+#include "../utils.h"
+
+#include <3ds.h>
+
+#include
+#include
+#include
+#include
+#include
+
+#define N3DS_ENABLE_LOGGING 0
+
+#if !N3DS_ENABLE_LOGGING
+#define fprintf(...) ((int) 0)
+#endif
+
+#define N3DS_MAX_NDSP_CHANNELS 24
+#define N3DS_MAX_SOUND_INSTANCES 24
+#define N3DS_RESERVED_STREAM_INSTANCES 4
+#define N3DS_SOUND_INSTANCE_ID_BASE 100000
+#define N3DS_AUDIO_STREAM_INDEX_BASE 200000
+#define N3DS_MAX_STREAMS 64
+#define N3DS_STREAM_BUFFER_COUNT 3
+#define N3DS_STREAM_CHUNK_SAMPLES (14u * 256u)
+#define N3DS_STREAM_ADPCM_CACHE_FRAMES 256u
+#define N3DS_STREAM_WORKER_STACK_SIZE (32u * 1024u)
+#define N3DS_STREAM_WORKER_PRIORITY 0x31
+#define N3DS_STREAM_WORKER_CORE_ID 1
+#define N3DS_STREAM_WORKER_CPU_LIMIT 20u
+#define N3DS_AUDIO_FILE_BUFFER_SIZE (64u * 1024u)
+#define N3DS_ENABLE_STREAM_WORKER 1
+#define N3DS_EAGER_SFX_PRELOAD 0
+#define N3DS_MAX_PRELOADED_SFX_BYTES_NEW3DS (15u * 1024u * 1024u)
+#define N3DS_MAX_PRELOADED_SFX_BYTES_OLD3DS (15u * 1024u * 1024u)
+#define N3DS_ROOM_PREWARM_SOUND_COUNT_NEW3DS 2u
+#define N3DS_ROOM_PREWARM_BYTE_BUDGET_NEW3DS (192u * 1024u)
+#define N3DS_BACKGROUND_PREWARM_SOUND_COUNT_NEW3DS 0u
+#define N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_NEW3DS 0u
+#define N3DS_ROOM_PREWARM_SOUND_COUNT_OLD3DS 1u
+#define N3DS_ROOM_PREWARM_BYTE_BUDGET_OLD3DS (96u * 1024u)
+#define N3DS_BACKGROUND_PREWARM_SOUND_COUNT_OLD3DS 0u
+#define N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_OLD3DS 0u
+#define N3DS_FORCE_PCM_BCWAV_PLAYBACK 0
+#define N3DS_ROMFS_AUDIO_BASE "romfs:/audio"
+#define N3DS_ROMFS_MUSIC_BASE "romfs:/"
+#define N3DS_SDMC_AUDIO_BASE "sdmc:/3ds/cinnamon/audio"
+#define N3DS_SDMC_MUSIC_BASE "sdmc:/3ds/cinnamon"
+#define N3DS_MAX_MISSING_AUDIO_PATHS 512
+#define N3DS_SOUND_BANK_MAGIC 0x314B4253u /* SBK1 */
+#define N3DS_SOUND_BANK_VERSION_BCWAV 1u
+#define N3DS_SOUND_BANK_VERSION_PCM16 2u
+#define N3DS_SOUND_BANK_ENTRY_CHANNEL_MASK 0x000000FFu
+#define N3DS_SOUND_BANK_ENTRY_FLAG_LOOP 0x00000100u
+#define N3DS_SOUND_BANK_ENTRY_FLAG_PCM16 0x00000200u
+#define N3DS_PCM_END_FADE_FRAMES 256u
+
+typedef struct {
+ uint8_t predictorScale;
+ int16_t yn1;
+ int16_t yn2;
+} N3DSDspContext;
+
+typedef struct {
+ uint16_t coefs[16];
+ uint32_t dataOffset;
+ uint32_t dataSize;
+ N3DSDspContext startContext;
+ N3DSDspContext loopContext;
+} N3DSBcwavChannel;
+
+typedef struct {
+ uint32_t sampleRate;
+ uint32_t sampleCount;
+ uint32_t loopStart;
+ uint32_t loopEnd;
+ bool loop;
+ uint8_t channelCount;
+ N3DSBcwavChannel channels[2];
+} N3DSBcwav;
+
+typedef struct {
+ bool active;
+ char* path;
+ uint8_t* blobData;
+ uint32_t blobSize;
+ N3DSBcwav bcwav;
+ float gain;
+ float pitch;
+ uint32_t refCount;
+} N3DSStreamEntry;
+
+typedef struct {
+ uint32_t nextFrameIndex;
+ uint8_t predictorScale;
+ int16_t hist1;
+ int16_t hist2;
+} N3DSStreamDecodeState;
+
+typedef struct {
+ uint32_t currentSample;
+ uint32_t pendingSamples;
+ uint32_t pendingOffset;
+ int16_t pendingPcm[14 * 2];
+ N3DSStreamDecodeState decode[2];
+ uint32_t fileCacheFrameBase[2];
+ uint32_t fileCacheFrameCount[2];
+ uint8_t fileCache[2][8u * N3DS_STREAM_ADPCM_CACHE_FRAMES];
+} N3DSStreamFillCursor;
+
+typedef struct {
+ bool requested;
+ bool inProgress;
+ bool ready;
+ bool success;
+ int bufferIndex;
+ uint32_t generation;
+ uint32_t bufferStartSample;
+ uint32_t sampleCount;
+ N3DSStreamFillCursor nextCursor;
+ ndspAdpcmData adpcmStates[2];
+} N3DSAsyncStreamFill;
+
+typedef struct {
+ bool active;
+ bool paused;
+ bool loop;
+ bool isStream;
+ bool streamFinished;
+ int32_t soundIndex;
+ int32_t instanceId;
+ int channelId;
+ int secondaryChannelId;
+ float gain;
+ float pitch;
+ float baseRate;
+ uint32_t sampleCount;
+ uint32_t sampleRate;
+ uint8_t channelCount;
+ bool useNativeAdpcm;
+ ndspWaveBuf waveBufs[N3DS_STREAM_BUFFER_COUNT];
+ ndspWaveBuf secondaryWaveBufs[N3DS_STREAM_BUFFER_COUNT];
+ ndspAdpcmData adpcmStates[N3DS_STREAM_BUFFER_COUNT * 2];
+ uint32_t bufferStartSample[N3DS_STREAM_BUFFER_COUNT];
+ uint8_t* streamAdpcm[2][N3DS_STREAM_BUFFER_COUNT];
+ int16_t* streamPcm[N3DS_STREAM_BUFFER_COUNT];
+ int16_t* pcmData;
+ FILE* streamFile;
+ const uint8_t* streamBlob;
+ uint32_t streamBlobSize;
+ bool ownsStreamBlob;
+ bool streamBlobLinear;
+ N3DSBcwav bcwav;
+ N3DSStreamDecodeState decode[2];
+ uint32_t fileCacheFrameBase[2];
+ uint32_t fileCacheFrameCount[2];
+ uint8_t fileCache[2][8u * N3DS_STREAM_ADPCM_CACHE_FRAMES];
+ uint32_t currentSample;
+ uint32_t pendingSamples;
+ uint32_t pendingOffset;
+ int16_t pendingPcm[14 * 2];
+ uint32_t streamGeneration;
+ N3DSAsyncStreamFill asyncFill;
+ u64 playbackStartTick;
+ u64 playbackReleaseTick;
+} N3DSSoundInstance;
+
+typedef struct {
+ bool attempted;
+ bool available;
+ bool headerAttempted;
+ bool headerAvailable;
+ bool pathAttempted;
+ bool blobOwned;
+ char* path;
+ uint8_t* blob;
+ uint8_t* nativeBlob;
+ uint32_t blobSize;
+ N3DSBcwav bcwav;
+} N3DSCachedSound;
+
+typedef struct {
+ uint32_t offset;
+ uint32_t size;
+ uint32_t sampleRate;
+ uint32_t sampleCount;
+ uint32_t flags;
+} N3DSPackedSoundBankEntry;
+
+struct N3DSAudioSystem {
+ AudioSystem base;
+ FileSystem* fileSystem;
+ bool initialized;
+ float masterGain;
+ N3DSStreamEntry streams[N3DS_MAX_STREAMS];
+ N3DSSoundInstance instances[N3DS_MAX_SOUND_INSTANCES];
+ N3DSCachedSound* cachedSounds;
+ uint32_t cachedSoundCount;
+ uint32_t cachedSoundBytes;
+ uint32_t cachedSoundPrewarmCursor;
+ uint32_t maxCachedSoundBytes;
+ uint32_t roomPrewarmSoundCount;
+ uint32_t roomPrewarmByteBudget;
+ uint32_t backgroundPrewarmSoundCount;
+ uint32_t backgroundPrewarmByteBudget;
+ uint32_t pendingPrewarmSoundCount;
+ uint32_t pendingPrewarmByteBudget;
+ bool isNew3DS;
+ int32_t lastResolveFailureSound;
+ bool ndspChannelInUse[N3DS_MAX_NDSP_CHANNELS];
+ uint8_t nextNdspChannel;
+ LightLock lock;
+ LightLock missingPathLock;
+ CondVar workerCond;
+ LightEvent workerEvent;
+ Thread workerThread;
+ bool workerStop;
+ bool workerEnabled;
+ uint8_t* packedSoundBankData;
+ uint32_t packedSoundBankSize;
+ N3DSPackedSoundBankEntry* packedSoundBankEntries;
+ uint32_t packedSoundBankEntryCount;
+ uint32_t packedSoundBankVersion;
+};
+
+static char* gN3DSMissingAudioPaths[N3DS_MAX_MISSING_AUDIO_PATHS];
+static uint32_t gN3DSMissingAudioPathCount = 0;
+
+static void N3DSAudio_cancelAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst);
+static bool N3DSAudio_hasActiveStreamPlaybackLocked(const N3DSAudioSystem* audio);
+static char* N3DSAudio_tryResolveSfxCandidate(N3DSAudioSystem* audio, const char* candidate);
+
+static void N3DSAudio_captureFillCursor(const N3DSSoundInstance* inst, N3DSStreamFillCursor* cursor) {
+ if (inst == NULL || cursor == NULL) return;
+ memset(cursor, 0, sizeof(*cursor));
+ cursor->currentSample = inst->currentSample;
+ cursor->pendingSamples = inst->pendingSamples;
+ cursor->pendingOffset = inst->pendingOffset;
+ memcpy(cursor->pendingPcm, inst->pendingPcm, sizeof(cursor->pendingPcm));
+ memcpy(cursor->decode, inst->decode, sizeof(cursor->decode));
+ memcpy(cursor->fileCacheFrameBase, inst->fileCacheFrameBase, sizeof(cursor->fileCacheFrameBase));
+ memcpy(cursor->fileCacheFrameCount, inst->fileCacheFrameCount, sizeof(cursor->fileCacheFrameCount));
+ memcpy(cursor->fileCache, inst->fileCache, sizeof(cursor->fileCache));
+}
+
+static void N3DSAudio_applyFillCursor(N3DSSoundInstance* inst, const N3DSStreamFillCursor* cursor) {
+ if (inst == NULL || cursor == NULL) return;
+ inst->currentSample = cursor->currentSample;
+ inst->pendingSamples = cursor->pendingSamples;
+ inst->pendingOffset = cursor->pendingOffset;
+ memcpy(inst->pendingPcm, cursor->pendingPcm, sizeof(inst->pendingPcm));
+ memcpy(inst->decode, cursor->decode, sizeof(inst->decode));
+ memcpy(inst->fileCacheFrameBase, cursor->fileCacheFrameBase, sizeof(inst->fileCacheFrameBase));
+ memcpy(inst->fileCacheFrameCount, cursor->fileCacheFrameCount, sizeof(inst->fileCacheFrameCount));
+ memcpy(inst->fileCache, cursor->fileCache, sizeof(inst->fileCache));
+}
+
+static uint16_t N3DSAudio_readU16(const uint8_t* ptr) {
+ return (uint16_t) (ptr[0] | (ptr[1] << 8));
+}
+
+static uint32_t N3DSAudio_readU32(const uint8_t* ptr) {
+ return (uint32_t) ptr[0] |
+ ((uint32_t) ptr[1] << 8) |
+ ((uint32_t) ptr[2] << 16) |
+ ((uint32_t) ptr[3] << 24);
+}
+
+static void N3DSAudio_softenPcmTail(int16_t* pcm, uint32_t sampleCount, uint8_t channelCount) {
+ if (pcm == NULL || sampleCount == 0 || channelCount == 0) return;
+
+ uint32_t fadeFrames = sampleCount;
+ if (fadeFrames > N3DS_PCM_END_FADE_FRAMES) fadeFrames = N3DS_PCM_END_FADE_FRAMES;
+ if (fadeFrames == 0) return;
+ if (fadeFrames == 1) {
+ for (uint8_t channel = 0; channel < channelCount; channel++) {
+ pcm[channel] = 0;
+ }
+ return;
+ }
+
+ uint32_t startFrame = sampleCount - fadeFrames;
+ for (uint32_t frame = 0; frame < fadeFrames; frame++) {
+ uint32_t gainNumerator = fadeFrames - 1u - frame;
+ uint32_t gainDenominator = fadeFrames - 1u;
+ uint32_t frameOffset = (startFrame + frame) * channelCount;
+ for (uint8_t channel = 0; channel < channelCount; channel++) {
+ int32_t sample = pcm[frameOffset + channel];
+ pcm[frameOffset + channel] = (int16_t) ((sample * (int32_t) gainNumerator) / (int32_t) gainDenominator);
+ }
+ }
+}
+
+static int16_t N3DSAudio_readS16(const uint8_t* ptr) {
+ return (int16_t) N3DSAudio_readU16(ptr);
+}
+
+static int N3DSAudio_signExtend4(int nibble) {
+ return (nibble & 0x8) ? (nibble - 16) : nibble;
+}
+
+static void N3DSAudio_configureFileBuffer(FILE* file) {
+ if (file == NULL) return;
+ setvbuf(file, NULL, _IOFBF, N3DS_AUDIO_FILE_BUFFER_SIZE);
+}
+
+static bool N3DSAudio_readFileFully(const char* path, uint8_t** outData, uint32_t* outSize) {
+ *outData = NULL;
+ *outSize = 0;
+
+ FILE* file = fopen(path, "rb");
+ if (file == NULL) return false;
+ N3DSAudio_configureFileBuffer(file);
+
+ fseek(file, 0, SEEK_END);
+ long size = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ if (size <= 0) {
+ fclose(file);
+ return false;
+ }
+
+ uint8_t* data = safeMalloc((size_t) size);
+ if (fread(data, 1, (size_t) size, file) != (size_t) size) {
+ fclose(file);
+ free(data);
+ return false;
+ }
+
+ fclose(file);
+ *outData = data;
+ *outSize = (uint32_t) size;
+ return true;
+}
+
+static bool N3DSAudio_readFileFullyLinear(const char* path, uint8_t** outData, uint32_t* outSize) {
+ *outData = NULL;
+ *outSize = 0;
+
+ FILE* file = fopen(path, "rb");
+ if (file == NULL) return false;
+ N3DSAudio_configureFileBuffer(file);
+
+ fseek(file, 0, SEEK_END);
+ long size = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ if (size <= 0) {
+ fclose(file);
+ return false;
+ }
+
+ uint8_t* data = linearAlloc((size_t) size);
+ if (data == NULL) {
+ fclose(file);
+ return false;
+ }
+ if (fread(data, 1, (size_t) size, file) != (size_t) size) {
+ fclose(file);
+ linearFree(data);
+ return false;
+ }
+
+ fclose(file);
+ DSP_FlushDataCache(data, (size_t) size);
+ *outData = data;
+ *outSize = (uint32_t) size;
+ return true;
+}
+
+static bool N3DSAudio_getFileSize(FILE* file, uint32_t* outSize) {
+ if (file == NULL || outSize == NULL) return false;
+
+ if (fseek(file, 0, SEEK_END) != 0) return false;
+ long size = ftell(file);
+ if (size <= 0) {
+ fseek(file, 0, SEEK_SET);
+ return false;
+ }
+ if (fseek(file, 0, SEEK_SET) != 0) return false;
+
+ *outSize = (uint32_t) size;
+ return true;
+}
+
+static uint8_t* N3DSAudio_cloneBlobToLinear(const uint8_t* data, uint32_t size) {
+ if (data == NULL || size == 0) return NULL;
+ uint8_t* linearData = linearAlloc(size);
+ if (linearData == NULL) return NULL;
+ memcpy(linearData, data, size);
+ DSP_FlushDataCache(linearData, size);
+ return linearData;
+}
+
+static const N3DSPackedSoundBankEntry* N3DSAudio_getPackedSoundEntry(const N3DSAudioSystem* audio, int32_t soundIndex) {
+ if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->packedSoundBankEntryCount) return NULL;
+ if (audio->packedSoundBankEntries == NULL) return NULL;
+ return &audio->packedSoundBankEntries[soundIndex];
+}
+
+static bool N3DSAudio_packedSoundEntryIsPcm16(const N3DSPackedSoundBankEntry* entry) {
+ return entry != NULL && (entry->flags & N3DS_SOUND_BANK_ENTRY_FLAG_PCM16) != 0;
+}
+
+static uint8_t N3DSAudio_packedSoundEntryChannelCount(const N3DSPackedSoundBankEntry* entry) {
+ return entry != NULL ? (uint8_t) (entry->flags & N3DS_SOUND_BANK_ENTRY_CHANNEL_MASK) : 0;
+}
+
+static bool N3DSAudio_getPackedSoundBlob(
+ const N3DSAudioSystem* audio,
+ int32_t soundIndex,
+ const uint8_t** outBlob,
+ uint32_t* outSize
+) {
+ if (outBlob != NULL) *outBlob = NULL;
+ if (outSize != NULL) *outSize = 0;
+ if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->packedSoundBankEntryCount) return false;
+ if (audio->packedSoundBankData == NULL || audio->packedSoundBankEntries == NULL) return false;
+
+ const N3DSPackedSoundBankEntry* entry = &audio->packedSoundBankEntries[soundIndex];
+ if (entry->size == 0) return false;
+ if (entry->offset >= audio->packedSoundBankSize) return false;
+ if (entry->size > audio->packedSoundBankSize - entry->offset) return false;
+
+ if (outBlob != NULL) *outBlob = audio->packedSoundBankData + entry->offset;
+ if (outSize != NULL) *outSize = entry->size;
+ return true;
+}
+
+static bool N3DSAudio_loadPackedSoundBank(N3DSAudioSystem* audio) {
+ if (audio == NULL) return false;
+ if (audio->packedSoundBankData != NULL) return true;
+
+ char* path = N3DSAudio_tryResolveSfxCandidate(audio, "sound_bank.bin");
+ if (path == NULL) return false;
+
+ uint8_t* bankData = NULL;
+ uint32_t bankSize = 0;
+ N3DSPackedSoundBankEntry* entries = NULL;
+ bool loaded = false;
+
+ if (!N3DSAudio_readFileFully(path, &bankData, &bankSize)) goto cleanup;
+ if (bankSize < 16u) goto cleanup;
+ if (N3DSAudio_readU32(bankData + 0u) != N3DS_SOUND_BANK_MAGIC) goto cleanup;
+ uint32_t version = N3DSAudio_readU32(bankData + 4u);
+ if (version != N3DS_SOUND_BANK_VERSION_BCWAV && version != N3DS_SOUND_BANK_VERSION_PCM16) goto cleanup;
+
+ uint32_t entryCount = N3DSAudio_readU32(bankData + 8u);
+ uint32_t dataOffset = N3DSAudio_readU32(bankData + 12u);
+ uint32_t entryStride = version >= N3DS_SOUND_BANK_VERSION_PCM16 ? 20u : 8u;
+ uint32_t tableBytes = entryCount * entryStride;
+ if (entryCount > 0 && tableBytes / entryStride != entryCount) goto cleanup;
+ if (dataOffset < 16u || dataOffset > bankSize) goto cleanup;
+ if (16u + tableBytes > dataOffset) goto cleanup;
+
+ entries = safeCalloc(entryCount, sizeof(N3DSPackedSoundBankEntry));
+ repeat(entryCount, i) {
+ uint32_t tableOffset = 16u + (uint32_t) i * entryStride;
+ uint32_t offset = N3DSAudio_readU32(bankData + tableOffset + 0u);
+ uint32_t size = N3DSAudio_readU32(bankData + tableOffset + 4u);
+ if (size != 0) {
+ if (offset < dataOffset || offset >= bankSize) goto cleanup;
+ if (size > bankSize - offset) goto cleanup;
+ }
+ entries[i].offset = offset;
+ entries[i].size = size;
+ if (version >= N3DS_SOUND_BANK_VERSION_PCM16) {
+ entries[i].sampleRate = N3DSAudio_readU32(bankData + tableOffset + 8u);
+ entries[i].sampleCount = N3DSAudio_readU32(bankData + tableOffset + 12u);
+ entries[i].flags = N3DSAudio_readU32(bankData + tableOffset + 16u);
+ }
+ }
+
+ audio->packedSoundBankData = bankData;
+ audio->packedSoundBankSize = bankSize;
+ audio->packedSoundBankEntries = entries;
+ audio->packedSoundBankEntryCount = entryCount;
+ audio->packedSoundBankVersion = version;
+ bankData = NULL;
+ entries = NULL;
+ loaded = true;
+ N3DSDebugLog_event(
+ "audio_bank",
+ "loaded version=%lu entries=%lu sizeKB=%lu path=%s",
+ (unsigned long) version,
+ (unsigned long) entryCount,
+ (unsigned long) (bankSize / 1024u),
+ path
+ );
+
+cleanup:
+ free(bankData);
+ free(entries);
+ free(path);
+ return loaded;
+}
+
+static bool N3DSAudio_fileExists(N3DSAudioSystem* audio, const char* path) {
+ if (path == NULL) return false;
+
+ if (audio != NULL) LightLock_Lock(&audio->missingPathLock);
+ repeat(gN3DSMissingAudioPathCount, i) {
+ if (strcmp(gN3DSMissingAudioPaths[i], path) == 0) {
+ if (audio != NULL) LightLock_Unlock(&audio->missingPathLock);
+ return false;
+ }
+ }
+ if (audio != NULL) LightLock_Unlock(&audio->missingPathLock);
+
+ struct stat st;
+ if (stat(path, &st) != 0) {
+ if (audio != NULL) LightLock_Lock(&audio->missingPathLock);
+ if (gN3DSMissingAudioPathCount < N3DS_MAX_MISSING_AUDIO_PATHS) {
+ gN3DSMissingAudioPaths[gN3DSMissingAudioPathCount++] = safeStrdup(path);
+ }
+ if (audio != NULL) LightLock_Unlock(&audio->missingPathLock);
+ return false;
+ }
+ return true;
+}
+
+static bool N3DSAudio_hasScheme(const char* path) {
+ return path != NULL && strstr(path, ":/") != NULL;
+}
+
+static bool N3DSAudio_pathStartsWith(const char* path, const char* prefix) {
+ if (path == NULL || prefix == NULL) return false;
+ size_t prefixLen = strlen(prefix);
+ return strncmp(path, prefix, prefixLen) == 0;
+}
+
+static char* N3DSAudio_joinPath(const char* base, const char* relativePath) {
+ if (base == NULL || relativePath == NULL) return NULL;
+ size_t baseLen = strlen(base);
+ size_t relLen = strlen(relativePath);
+ bool needSlash = baseLen > 0 && base[baseLen - 1] != '/' && relLen > 0 && relativePath[0] != '/';
+ char* result = safeMalloc(baseLen + relLen + (needSlash ? 2 : 1));
+ memcpy(result, base, baseLen);
+ size_t cursor = baseLen;
+ if (needSlash) result[cursor++] = '/';
+ memcpy(result + cursor, relativePath, relLen);
+ result[cursor + relLen] = '\0';
+ return result;
+}
+
+static char* N3DSAudio_tryResolveCandidateInBases(N3DSAudioSystem* audio, const char* candidate, const char* const* bases, size_t baseCount) {
+ (void) audio;
+ if (candidate == NULL || candidate[0] == '\0') return NULL;
+ if (N3DSAudio_hasScheme(candidate)) {
+ return N3DSAudio_fileExists(audio, candidate) ? safeStrdup(candidate) : NULL;
+ }
+
+ repeat(baseCount, i) {
+ char* resolved = N3DSAudio_joinPath(bases[i], candidate);
+ if (resolved != NULL && N3DSAudio_fileExists(audio, resolved)) {
+ return resolved;
+ }
+ free(resolved);
+ }
+ return NULL;
+}
+
+static char* N3DSAudio_tryResolveCandidate(N3DSAudioSystem* audio, const char* candidate) {
+ const char* bases[] = {
+ N3DS_ROMFS_AUDIO_BASE,
+ N3DS_ROMFS_MUSIC_BASE,
+ N3DS_SDMC_AUDIO_BASE,
+ N3DS_SDMC_MUSIC_BASE,
+ };
+ return N3DSAudio_tryResolveCandidateInBases(audio, candidate, bases, 4);
+}
+
+static char* N3DSAudio_tryResolveMusicCandidate(N3DSAudioSystem* audio, const char* candidate) {
+ const char* bases[] = {
+ N3DS_ROMFS_MUSIC_BASE,
+ N3DS_ROMFS_AUDIO_BASE,
+ N3DS_SDMC_MUSIC_BASE,
+ N3DS_SDMC_AUDIO_BASE,
+ };
+ return N3DSAudio_tryResolveCandidateInBases(audio, candidate, bases, 4);
+}
+
+static char* N3DSAudio_tryResolveSfxCandidate(N3DSAudioSystem* audio, const char* candidate) {
+ const char* bases[] = {
+ N3DS_ROMFS_AUDIO_BASE,
+ N3DS_SDMC_AUDIO_BASE,
+ };
+ return N3DSAudio_tryResolveCandidateInBases(audio, candidate, bases, 2);
+}
+
+static char* N3DSAudio_resolveStreamPath(N3DSAudioSystem* audio, const char* name) {
+ if (name == NULL || name[0] == '\0') return NULL;
+
+ char* resolved = N3DSAudio_tryResolveMusicCandidate(audio, name);
+ if (resolved != NULL) return resolved;
+
+ char candidate[512];
+ if (strchr(name, '.') == NULL) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", name);
+ return N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ }
+
+ const char* slash = strrchr(name, '/');
+ const char* base = slash != NULL ? slash + 1 : name;
+ const char* dot = strrchr(base, '.');
+ if (dot == NULL || dot <= base) return NULL;
+
+ size_t dirLen = slash != NULL ? (size_t) (slash - name + 1) : 0;
+ size_t baseLen = (size_t) (dot - base);
+
+ if (dirLen + baseLen + 7 < sizeof(candidate)) {
+ memcpy(candidate, name, dirLen);
+ memcpy(candidate + dirLen, base, baseLen);
+ memcpy(candidate + dirLen + baseLen, ".bcwav", 7);
+ resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ if (baseLen + 7 < sizeof(candidate)) {
+ memcpy(candidate, base, baseLen);
+ memcpy(candidate + baseLen, ".bcwav", 7);
+ resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ return NULL;
+}
+
+static char* N3DSAudio_resolveSoundPath(N3DSAudioSystem* audio, const Sound* sound, int32_t soundIndex);
+static N3DSCachedSound* N3DSAudio_getCachedSoundIfAvailable(N3DSAudioSystem* audio, int32_t soundIndex);
+static const char* N3DSAudio_getResolvedCachedSoundPath(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound);
+static bool N3DSAudio_getCachedSoundHeader(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound, N3DSBcwav* out);
+static uint8_t* N3DSAudio_getCachedNativeBlob(N3DSCachedSound* cachedSound);
+static bool N3DSAudio_primeNativeAdpcm(N3DSSoundInstance* inst);
+static void N3DSAudio_prewarmSoundCache(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget);
+static void N3DSAudio_requestCachePrewarmLocked(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget);
+static bool N3DSAudio_loadPackedSoundBank(N3DSAudioSystem* audio);
+static bool N3DSAudio_getPackedSoundBlob(
+ const N3DSAudioSystem* audio,
+ int32_t soundIndex,
+ const uint8_t** outBlob,
+ uint32_t* outSize
+);
+
+static bool N3DSAudio_stringContainsIgnoreCase(const char* haystack, const char* needle) {
+ if (haystack == NULL || needle == NULL || needle[0] == '\0') return false;
+ size_t needleLen = strlen(needle);
+ for (const char* cursor = haystack; *cursor != '\0'; ++cursor) {
+ size_t matched = 0;
+ while (matched < needleLen) {
+ char a = cursor[matched];
+ if (a == '\0') return false;
+ char b = needle[matched];
+ if (a >= 'A' && a <= 'Z') a = (char) (a - 'A' + 'a');
+ if (b >= 'A' && b <= 'Z') b = (char) (b - 'A' + 'a');
+ if (a != b) break;
+ matched++;
+ }
+ if (matched == needleLen) return true;
+ }
+ return false;
+}
+
+static bool N3DSAudio_nameLooksLikeSfx(const char* value) {
+ if (value == NULL || value[0] == '\0') return false;
+
+ const char* base = strrchr(value, '/');
+ const char* backslash = strrchr(value, '\\');
+ if (backslash != NULL && (base == NULL || backslash > base)) base = backslash;
+ base = base != NULL ? base + 1 : value;
+
+ return strncmp(base, "mus_sfx", 7) == 0 ||
+ strncmp(base, "sfx_", 4) == 0 ||
+ strncmp(base, "snd_", 4) == 0 ||
+ N3DSAudio_stringContainsIgnoreCase(base, "_sfx") ||
+ N3DSAudio_stringContainsIgnoreCase(base, "sfx_");
+}
+
+static bool N3DSAudio_nameLooksLikeMusic(const char* value) {
+ if (value == NULL || value[0] == '\0') return false;
+
+ const char* base = strrchr(value, '/');
+ const char* backslash = strrchr(value, '\\');
+ if (backslash != NULL && (base == NULL || backslash > base)) base = backslash;
+ base = base != NULL ? base + 1 : value;
+ if (N3DSAudio_nameLooksLikeSfx(base)) return false;
+ if (strncmp(base, "mus_", 4) == 0 || strncmp(base, "bgm_", 4) == 0) return true;
+ return N3DSAudio_stringContainsIgnoreCase(base, "music");
+}
+
+static bool N3DSAudio_soundLooksLikeSfx(const Sound* sound) {
+ if (sound == NULL) return false;
+ return N3DSAudio_nameLooksLikeSfx(sound->name) ||
+ N3DSAudio_nameLooksLikeSfx(sound->file) ||
+ N3DSAudio_stringContainsIgnoreCase(sound->type, "sfx") ||
+ N3DSAudio_stringContainsIgnoreCase(sound->type, "effect");
+}
+
+static bool N3DSAudio_soundLooksLikeMusic(const Sound* sound) {
+ if (sound == NULL) return false;
+ if (N3DSAudio_soundLooksLikeSfx(sound)) return false;
+ return N3DSAudio_nameLooksLikeMusic(sound->name) ||
+ N3DSAudio_nameLooksLikeMusic(sound->file) ||
+ N3DSAudio_stringContainsIgnoreCase(sound->type, "music") ||
+ N3DSAudio_stringContainsIgnoreCase(sound->type, "stream");
+}
+
+static bool N3DSAudio_pathLooksLikeBundledMusic(const char* path) {
+ if (path == NULL) return false;
+ if (N3DSAudio_pathStartsWith(path, "romfs:/audio/")) return false;
+ if (N3DSAudio_pathStartsWith(path, "sdmc:/3ds/cinnamon/audio/")) return false;
+ return N3DSAudio_pathStartsWith(path, "romfs:/") || N3DSAudio_pathStartsWith(path, "sdmc:/3ds/cinnamon/");
+}
+
+static bool N3DSAudio_extractBaseNameNoExt(const char* value, char* out, size_t outSize) {
+ if (out == NULL || outSize == 0) return false;
+ out[0] = '\0';
+ if (value == NULL || value[0] == '\0') return false;
+
+ const char* base = strrchr(value, '/');
+ const char* backslash = strrchr(value, '\\');
+ if (backslash != NULL && (base == NULL || backslash > base)) base = backslash;
+ base = (base != NULL) ? base + 1 : value;
+ if (base[0] == '\0') return false;
+
+ const char* dot = strrchr(base, '.');
+ size_t len = (dot != NULL && dot > base) ? (size_t) (dot - base) : strlen(base);
+ if (len == 0 || len + 1 > outSize) return false;
+ memcpy(out, base, len);
+ out[len] = '\0';
+ return true;
+}
+
+static bool N3DSAudio_shouldPreloadSound(const Sound* sound) {
+ return sound != NULL;
+}
+
+static bool N3DSAudio_shouldPrewarmCachedSound(const Sound* sound, const N3DSBcwav* bcwav) {
+ if (sound == NULL || bcwav == NULL) return false;
+ if (!N3DSAudio_soundLooksLikeMusic(sound) &&
+ bcwav->sampleRate > 0 &&
+ bcwav->sampleCount > (bcwav->sampleRate * 10u)) {
+ return false;
+ }
+ return true;
+}
+
+static bool N3DSAudio_parseBcwavInfo(const uint8_t* info, uint32_t infoSize, uint32_t dataBlockOffset, uint32_t fileSize, N3DSBcwav* out) {
+ memset(out, 0, sizeof(*out));
+ if (infoSize < 0x20 || memcmp(info, "INFO", 4) != 0) return false;
+
+ uint8_t encoding = info[0x08];
+ if (encoding != 2) return false;
+
+ out->loop = info[0x09] != 0;
+ out->sampleRate = N3DSAudio_readU32(info + 0x0C);
+ out->loopStart = N3DSAudio_readU32(info + 0x10);
+ out->loopEnd = N3DSAudio_readU32(info + 0x14);
+ out->sampleCount = out->loopEnd;
+
+ uint32_t tableOffset = 0x1C;
+ uint32_t channelCount = N3DSAudio_readU32(info + tableOffset);
+ if (channelCount == 0 || channelCount > 2) return false;
+ if (infoSize < tableOffset + 4 + channelCount * 8) return false;
+
+ out->channelCount = (uint8_t) channelCount;
+ uint32_t encodedBytes = ((out->sampleCount + 13u) / 14u) * 8u;
+
+ repeat(channelCount, i) {
+ const uint8_t* channelRef = info + tableOffset + 4 + i * 8;
+ uint32_t channelInfoOffset = tableOffset + N3DSAudio_readU32(channelRef + 4);
+ if (channelInfoOffset + 0x14 > infoSize) return false;
+
+ const uint8_t* channelInfo = info + channelInfoOffset;
+ uint32_t sampleOffset = N3DSAudio_readU32(channelInfo + 4);
+ uint32_t adpcmInfoOffset = channelInfoOffset + N3DSAudio_readU32(channelInfo + 12);
+ if (adpcmInfoOffset + 0x2E > infoSize) return false;
+
+ N3DSBcwavChannel* channel = &out->channels[i];
+ channel->dataOffset = dataBlockOffset + 8 + sampleOffset;
+ channel->dataSize = encodedBytes;
+ if (channel->dataOffset + channel->dataSize > fileSize) return false;
+
+ const uint8_t* adpcmInfo = info + adpcmInfoOffset;
+ repeat(16, coef) {
+ channel->coefs[coef] = N3DSAudio_readU16(adpcmInfo + coef * 2);
+ }
+ channel->startContext.predictorScale = adpcmInfo[0x20];
+ channel->startContext.yn1 = N3DSAudio_readS16(adpcmInfo + 0x22);
+ channel->startContext.yn2 = N3DSAudio_readS16(adpcmInfo + 0x24);
+ channel->loopContext.predictorScale = adpcmInfo[0x26];
+ channel->loopContext.yn1 = N3DSAudio_readS16(adpcmInfo + 0x28);
+ channel->loopContext.yn2 = N3DSAudio_readS16(adpcmInfo + 0x2A);
+ }
+
+ return true;
+}
+
+static bool N3DSAudio_parseBcwavBlob(const uint8_t* data, uint32_t size, N3DSBcwav* out) {
+ if (size < 0x40 || memcmp(data, "CWAV", 4) != 0) return false;
+ uint32_t infoOffset = N3DSAudio_readU32(data + 0x18);
+ uint32_t infoSize = N3DSAudio_readU32(data + 0x1C);
+ uint32_t dataOffset = N3DSAudio_readU32(data + 0x24);
+ if (infoOffset + infoSize > size || dataOffset + 8 > size) return false;
+ return N3DSAudio_parseBcwavInfo(data + infoOffset, infoSize, dataOffset, size, out);
+}
+
+static bool N3DSAudio_parseBcwavFile(const char* path, N3DSBcwav* out) {
+ if (path == NULL || out == NULL) return false;
+
+ bool success = false;
+ FILE* file = fopen(path, "rb");
+ if (file == NULL) return false;
+ N3DSAudio_configureFileBuffer(file);
+
+ uint32_t fileSize = 0;
+ uint8_t header[0x28];
+ uint8_t* infoData = NULL;
+
+ if (!N3DSAudio_getFileSize(file, &fileSize)) goto cleanup;
+ if (fileSize < 0x40) goto cleanup;
+ if (fread(header, 1, sizeof(header), file) != sizeof(header)) goto cleanup;
+ if (memcmp(header, "CWAV", 4) != 0) goto cleanup;
+
+ uint32_t infoOffset = N3DSAudio_readU32(header + 0x18);
+ uint32_t infoSize = N3DSAudio_readU32(header + 0x1C);
+ uint32_t dataOffset = N3DSAudio_readU32(header + 0x24);
+ if (infoSize == 0 || infoOffset + infoSize > fileSize || dataOffset + 8 > fileSize) goto cleanup;
+
+ infoData = safeMalloc(infoSize);
+ if (fseek(file, (long) infoOffset, SEEK_SET) != 0) goto cleanup;
+ if (fread(infoData, 1, infoSize, file) != infoSize) goto cleanup;
+
+ success = N3DSAudio_parseBcwavInfo(infoData, infoSize, dataOffset, fileSize, out);
+
+cleanup:
+ free(infoData);
+ fclose(file);
+ return success;
+}
+
+static void N3DSAudio_decodeFrame(const N3DSBcwavChannel* channel, const uint8_t* frame, int16_t* hist1, int16_t* hist2, int16_t outSamples[14]) {
+ int predictor = frame[0] >> 4;
+ int scale = 1 << (frame[0] & 0x0F);
+ int coef1 = (int16_t) channel->coefs[predictor * 2 + 0];
+ int coef2 = (int16_t) channel->coefs[predictor * 2 + 1];
+
+ int sampleIndex = 0;
+ for (int byteIndex = 1; byteIndex < 8; ++byteIndex) {
+ int hi = N3DSAudio_signExtend4(frame[byteIndex] >> 4);
+ int lo = N3DSAudio_signExtend4(frame[byteIndex] & 0x0F);
+ int nibbles[2] = { hi, lo };
+ repeat(2, nibbleIndex) {
+ int sample = (nibbles[nibbleIndex] * scale) << 11;
+ sample += 1024 + coef1 * (*hist1) + coef2 * (*hist2);
+ sample >>= 11;
+ if (sample > 32767) sample = 32767;
+ if (sample < -32768) sample = -32768;
+ outSamples[sampleIndex++] = (int16_t) sample;
+ *hist2 = *hist1;
+ *hist1 = (int16_t) sample;
+ }
+ }
+}
+
+static N3DSDspContext N3DSAudio_contextForSample(const N3DSBcwavChannel* channel, const N3DSStreamDecodeState* state, uint32_t sampleIndex) {
+ N3DSDspContext context;
+ if (channel != NULL && sampleIndex == 0) {
+ context = channel->startContext;
+ } else {
+ context.predictorScale = state->predictorScale;
+ context.yn1 = state->hist1;
+ context.yn2 = state->hist2;
+ }
+ return context;
+}
+
+static int16_t* N3DSAudio_decodeBcwavToPcm(const uint8_t* fileData, const N3DSBcwav* bcwav) {
+ size_t totalSamples = (size_t) bcwav->sampleCount * bcwav->channelCount;
+ int16_t* pcm = linearAlloc(totalSamples * sizeof(int16_t));
+ if (pcm == NULL) return NULL;
+
+ int16_t frameSamples[2][14];
+ int16_t hist1[2] = {
+ bcwav->channels[0].startContext.yn1,
+ bcwav->channels[1].startContext.yn1,
+ };
+ int16_t hist2[2] = {
+ bcwav->channels[0].startContext.yn2,
+ bcwav->channels[1].startContext.yn2,
+ };
+
+ uint32_t frameCount = (bcwav->sampleCount + 13u) / 14u;
+ uint32_t sampleCursor = 0;
+ repeat(frameCount, frameIndex) {
+ repeat(bcwav->channelCount, channelIndex) {
+ const uint8_t* frame = fileData + bcwav->channels[channelIndex].dataOffset + frameIndex * 8u;
+ N3DSAudio_decodeFrame(&bcwav->channels[channelIndex], frame, &hist1[channelIndex], &hist2[channelIndex], frameSamples[channelIndex]);
+ }
+
+ uint32_t samplesThisFrame = bcwav->sampleCount - sampleCursor;
+ if (samplesThisFrame > 14u) samplesThisFrame = 14u;
+ repeat(samplesThisFrame, sampleIndex) {
+ if (bcwav->channelCount == 1) {
+ pcm[sampleCursor + sampleIndex] = frameSamples[0][sampleIndex];
+ } else {
+ size_t outIndex = ((size_t) sampleCursor + sampleIndex) * 2u;
+ pcm[outIndex + 0] = frameSamples[0][sampleIndex];
+ pcm[outIndex + 1] = frameSamples[1][sampleIndex];
+ }
+ }
+ sampleCursor += samplesThisFrame;
+ }
+
+ DSP_FlushDataCache(pcm, totalSamples * sizeof(int16_t));
+ return pcm;
+}
+
+static char* N3DSAudio_tryResolveGeneratedSoundPath(N3DSAudioSystem* audio, const Sound* sound, int32_t soundIndex) {
+ char candidate[512];
+ bool isMusic = N3DSAudio_soundLooksLikeMusic(sound);
+ char baseName[256];
+
+ if (isMusic) {
+ if (sound != NULL && N3DSAudio_extractBaseNameNoExt(sound->file, baseName, sizeof(baseName))) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName);
+ char* resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ if (sound != NULL && N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName);
+ char* resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ }
+
+ if (sound != NULL && sound->name != NULL && sound->name[0] != '\0') {
+ if (!isMusic) {
+ snprintf(candidate, sizeof(candidate), "audio/%s.bcwav", sound->name);
+ char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", sound->name);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ }
+
+ if (!isMusic && sound != NULL) {
+ if (N3DSAudio_extractBaseNameNoExt(sound->file, baseName, sizeof(baseName))) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName);
+ char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ if (N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName);
+ char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ }
+
+ if (!isMusic && soundIndex >= 0) {
+ snprintf(candidate, sizeof(candidate), "audio/sound_%05ld.bcwav", (long) soundIndex);
+ char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+
+ snprintf(candidate, sizeof(candidate), "sound_%05ld.bcwav", (long) soundIndex);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+
+ snprintf(candidate, sizeof(candidate), "audio/sound_%ld.bcwav", (long) soundIndex);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+
+ snprintf(candidate, sizeof(candidate), "sound_%ld.bcwav", (long) soundIndex);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ if (!isMusic && sound != NULL && sound->audioFile >= 0) {
+ snprintf(candidate, sizeof(candidate), "audio/audo_%05ld.bcwav", (long) sound->audioFile);
+ char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+
+ snprintf(candidate, sizeof(candidate), "audo_%05ld.bcwav", (long) sound->audioFile);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ if (sound != NULL && sound->name != NULL && sound->name[0] != '\0') {
+ char baseName[256];
+ if (N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName);
+ char* resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ }
+
+ return NULL;
+}
+
+static char* N3DSAudio_resolveAudioFilePath(N3DSAudioSystem* audio, const char* name) {
+ if (name == NULL || name[0] == '\0') return NULL;
+
+ char* resolved = N3DSAudio_resolveStreamPath(audio, name);
+ if (resolved != NULL) return resolved;
+
+ char candidate[512];
+ if (strchr(name, '.') == NULL) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", name);
+ return N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ }
+
+ char baseName[256];
+ if (N3DSAudio_extractBaseNameNoExt(name, baseName, sizeof(baseName))) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName);
+ resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ return NULL;
+}
+
+static char* N3DSAudio_resolveSoundPath(N3DSAudioSystem* audio, const Sound* sound, int32_t soundIndex) {
+ const char* name = sound != NULL ? sound->file : NULL;
+ if (name == NULL || name[0] == '\0') return NULL;
+ bool isMusic = N3DSAudio_soundLooksLikeMusic(sound);
+
+ char candidate[512];
+ char* resolved = NULL;
+
+ if (strchr(name, '.') == NULL) {
+ if (isMusic) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", name);
+ resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ } else {
+ snprintf(candidate, sizeof(candidate), "audio/%s.bcwav", name);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", name);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ return NULL;
+ }
+
+ resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, name) : N3DSAudio_tryResolveSfxCandidate(audio, name);
+ if (resolved != NULL) return resolved;
+
+ const char* slash = strrchr(name, '/');
+ const char* base = slash != NULL ? slash + 1 : name;
+ const char* dot = strrchr(base, '.');
+ if (dot != NULL && dot > base) {
+ size_t dirLen = slash != NULL ? (size_t) (slash - name + 1) : 0;
+ size_t baseLen = (size_t) (dot - base);
+
+ if (dirLen + baseLen + 7 < sizeof(candidate)) {
+ memcpy(candidate, name, dirLen);
+ memcpy(candidate + dirLen, base, baseLen);
+ memcpy(candidate + dirLen + baseLen, ".bcwav", 7);
+ resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ if (!isMusic && baseLen + 13 < sizeof(candidate)) {
+ memcpy(candidate, "audio/", 6);
+ memcpy(candidate + 6, base, baseLen);
+ memcpy(candidate + 6 + baseLen, ".bcwav", 7);
+ resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+
+ if (baseLen + 7 < sizeof(candidate)) {
+ memcpy(candidate, base, baseLen);
+ memcpy(candidate + baseLen, ".bcwav", 7);
+ resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ }
+
+ if (sound != NULL && sound->name != NULL && sound->name[0] != '\0') {
+ char baseName[256];
+ if (N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) {
+ snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName);
+ resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate);
+ if (resolved != NULL) return resolved;
+ }
+ }
+
+ (void) soundIndex;
+ return NULL;
+}
+
+static N3DSSoundInstance* N3DSAudio_findInstanceById(N3DSAudioSystem* audio, int32_t instanceId) {
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].instanceId == instanceId) {
+ return &audio->instances[i];
+ }
+ }
+ return NULL;
+}
+
+static bool N3DSAudio_isInstanceId(int32_t soundOrInstance) {
+ return soundOrInstance >= N3DS_SOUND_INSTANCE_ID_BASE &&
+ soundOrInstance < N3DS_SOUND_INSTANCE_ID_BASE + N3DS_MAX_SOUND_INSTANCES;
+}
+
+static int32_t N3DSAudio_streamSlotFromSoundIndex(int32_t soundIndex) {
+ int32_t slot = soundIndex - N3DS_AUDIO_STREAM_INDEX_BASE;
+ return (slot >= 0 && slot < N3DS_MAX_STREAMS) ? slot : -1;
+}
+
+static int N3DSAudio_acquireChannel(N3DSAudioSystem* audio) {
+ if (audio == NULL) return -1;
+
+ uint32_t start = (uint32_t) audio->nextNdspChannel % N3DS_MAX_NDSP_CHANNELS;
+ repeat(N3DS_MAX_NDSP_CHANNELS, offset) {
+ uint32_t channel = (start + offset) % N3DS_MAX_NDSP_CHANNELS;
+ if (!audio->ndspChannelInUse[channel]) {
+ audio->ndspChannelInUse[channel] = true;
+ audio->nextNdspChannel = (uint8_t) ((channel + 1u) % N3DS_MAX_NDSP_CHANNELS);
+ return (int) channel;
+ }
+ }
+ return -1;
+}
+
+static void N3DSAudio_resetChannelPlayback(int channelId) {
+ if (channelId < 0 || channelId >= N3DS_MAX_NDSP_CHANNELS) return;
+ ndspChnSetPaused(channelId, true);
+ ndspChnWaveBufClear(channelId);
+ ndspChnReset(channelId);
+ ndspChnSetInterp(channelId, NDSP_INTERP_LINEAR);
+}
+
+static void N3DSAudio_releaseChannel(N3DSAudioSystem* audio, int channelId) {
+ if (channelId < 0 || channelId >= N3DS_MAX_NDSP_CHANNELS) return;
+ N3DSAudio_resetChannelPlayback(channelId);
+ audio->ndspChannelInUse[channelId] = false;
+}
+
+static uint32_t N3DSAudio_adpcmByteOffsetForSample(uint32_t sampleIndex) {
+ return (sampleIndex / 14u) * 8u;
+}
+
+static bool N3DSAudio_isFrameAlignedSample(uint32_t sampleIndex) {
+ return (sampleIndex % 14u) == 0;
+}
+
+static float N3DSAudio_sanitizeGain(float gain) {
+ if (!isfinite(gain)) return 1.0f;
+ if (gain < 0.0f) return 0.0f;
+ if (gain > 1.0f) return 1.0f;
+ return gain;
+}
+
+static float N3DSAudio_sanitizePitch(float pitch) {
+ if (!isfinite(pitch) || pitch <= 0.0f) return 1.0f;
+ if (pitch < 0.0625f) return 0.0625f;
+ if (pitch > 4.0f) return 4.0f;
+ return pitch;
+}
+
+static void N3DSAudio_refreshNonStreamReleaseTick(N3DSSoundInstance* inst) {
+ if (inst == NULL || inst->isStream || inst->loop || inst->sampleRate == 0 || inst->sampleCount == 0) {
+ return;
+ }
+
+ float pitch = N3DSAudio_sanitizePitch(inst->pitch);
+ double durationSeconds = ((double) inst->sampleCount / (double) inst->sampleRate) / (double) pitch;
+ if (!(durationSeconds > 0.0)) durationSeconds = 0.001;
+
+ u64 durationTicks = (u64) (durationSeconds * (double) SYSCLOCK_ARM11);
+ if (durationTicks == 0) durationTicks = 1;
+ inst->playbackReleaseTick = inst->playbackStartTick + durationTicks;
+}
+
+static bool N3DSAudio_nonStreamPlaybackFinished(const N3DSSoundInstance* inst, u64 nowTick) {
+ if (inst == NULL || !inst->active || inst->isStream || inst->loop || inst->paused) return false;
+ if (nowTick < inst->playbackReleaseTick) return false;
+ if (inst->waveBufs[0].status != NDSP_WBUF_DONE) return false;
+ if (ndspChnIsPlaying(inst->channelId)) return false;
+ if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ if (inst->secondaryWaveBufs[0].status != NDSP_WBUF_DONE) return false;
+ if (ndspChnIsPlaying(inst->secondaryChannelId)) return false;
+ }
+ return true;
+}
+
+static void N3DSAudio_applyMix(N3DSAudioSystem* audio, N3DSSoundInstance* inst) {
+ float mix[12];
+ memset(mix, 0, sizeof(mix));
+ inst->gain = N3DSAudio_sanitizeGain(inst->gain);
+ if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ mix[0] = inst->gain * audio->masterGain;
+ ndspChnSetMix(inst->channelId, mix);
+
+ memset(mix, 0, sizeof(mix));
+ mix[1] = inst->gain * audio->masterGain;
+ ndspChnSetMix(inst->secondaryChannelId, mix);
+ } else {
+ mix[0] = inst->gain * audio->masterGain;
+ mix[1] = inst->gain * audio->masterGain;
+ ndspChnSetMix(inst->channelId, mix);
+ }
+
+ float pitch = N3DSAudio_sanitizePitch(inst->pitch);
+ float rate = inst->baseRate * pitch;
+ if (!isfinite(rate) || rate < 4000.0f) rate = 4000.0f;
+ if (rate > 96000.0f) rate = 96000.0f;
+ inst->pitch = pitch;
+ ndspChnSetRate(inst->channelId, rate);
+ if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ ndspChnSetRate(inst->secondaryChannelId, rate);
+ }
+}
+
+static void N3DSAudio_resetInstanceWaveBufState(N3DSSoundInstance* inst) {
+ if (inst == NULL) return;
+ memset(inst->waveBufs, 0, sizeof(inst->waveBufs));
+ memset(inst->secondaryWaveBufs, 0, sizeof(inst->secondaryWaveBufs));
+ memset(inst->adpcmStates, 0, sizeof(inst->adpcmStates));
+ memset(inst->bufferStartSample, 0, sizeof(inst->bufferStartSample));
+}
+
+static void N3DSAudio_rebuildInstanceChannels(N3DSAudioSystem* audio, N3DSSoundInstance* inst) {
+ if (audio == NULL || inst == NULL || inst->channelId < 0) return;
+
+ N3DSAudio_resetChannelPlayback(inst->channelId);
+ if (inst->secondaryChannelId >= 0) {
+ N3DSAudio_resetChannelPlayback(inst->secondaryChannelId);
+ }
+
+ if (inst->useNativeAdpcm) {
+ ndspChnSetFormat(inst->channelId, NDSP_FORMAT_MONO_ADPCM);
+ ndspChnSetAdpcmCoefs(inst->channelId, inst->bcwav.channels[0].coefs);
+ if (inst->secondaryChannelId >= 0) {
+ ndspChnSetFormat(inst->secondaryChannelId, NDSP_FORMAT_MONO_ADPCM);
+ ndspChnSetAdpcmCoefs(inst->secondaryChannelId, inst->bcwav.channels[1].coefs);
+ }
+ } else {
+ ndspChnSetFormat(inst->channelId, inst->channelCount == 2 ? NDSP_FORMAT_STEREO_PCM16 : NDSP_FORMAT_MONO_PCM16);
+ }
+
+ N3DSAudio_applyMix(audio, inst);
+ ndspChnSetPaused(inst->channelId, inst->paused);
+ if (inst->secondaryChannelId >= 0) {
+ ndspChnSetPaused(inst->secondaryChannelId, inst->paused);
+ }
+}
+
+static void N3DSAudio_releaseCachedSound(N3DSCachedSound* cachedSound) {
+ if (cachedSound == NULL) return;
+ free(cachedSound->path);
+ if (cachedSound->blobOwned) free(cachedSound->blob);
+ if (cachedSound->nativeBlob != NULL) linearFree(cachedSound->nativeBlob);
+ memset(cachedSound, 0, sizeof(*cachedSound));
+}
+
+static uint8_t* N3DSAudio_getCachedNativeBlob(N3DSCachedSound* cachedSound) {
+ if (cachedSound == NULL || !cachedSound->available || cachedSound->blob == NULL || cachedSound->blobSize == 0) return NULL;
+ if (cachedSound->nativeBlob != NULL) return cachedSound->nativeBlob;
+
+ cachedSound->nativeBlob = N3DSAudio_cloneBlobToLinear(cachedSound->blob, cachedSound->blobSize);
+ return cachedSound->nativeBlob;
+}
+
+static bool N3DSAudio_tryCacheSoundBlob(N3DSAudioSystem* audio, int32_t soundIndex) {
+ if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return false;
+ N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex];
+ if (cachedSound->attempted) return cachedSound->available;
+
+ cachedSound->attempted = true;
+
+ DataWin* dw = audio->base.audioGroups[0];
+ Sound* sound = &dw->sond.sounds[soundIndex];
+ if (!N3DSAudio_shouldPreloadSound(sound)) return false;
+
+ const uint8_t* packedBlob = NULL;
+ uint32_t packedBlobSize = 0;
+ const N3DSPackedSoundBankEntry* packedEntry = N3DSAudio_getPackedSoundEntry(audio, soundIndex);
+ if (!N3DSAudio_soundLooksLikeMusic(sound) &&
+ packedEntry != NULL &&
+ !N3DSAudio_packedSoundEntryIsPcm16(packedEntry) &&
+ N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize)) {
+ N3DSBcwav bcwav;
+ if (!N3DSAudio_parseBcwavBlob(packedBlob, packedBlobSize, &bcwav)) return false;
+ if (!N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) return false;
+
+ cachedSound->available = true;
+ cachedSound->headerAttempted = true;
+ cachedSound->headerAvailable = true;
+ cachedSound->blobOwned = false;
+ cachedSound->blob = (uint8_t*) packedBlob;
+ cachedSound->blobSize = packedBlobSize;
+ cachedSound->bcwav = bcwav;
+ return true;
+ }
+
+ const char* resolvedPath = N3DSAudio_getResolvedCachedSoundPath(audio, soundIndex, sound);
+ if (resolvedPath == NULL) return false;
+ char* path = safeStrdup(resolvedPath);
+
+ uint8_t* blob = NULL;
+ uint32_t blobSize = 0;
+ if (!N3DSAudio_readFileFully(path, &blob, &blobSize)) {
+ free(path);
+ return false;
+ }
+ if (audio->cachedSoundBytes + blobSize > audio->maxCachedSoundBytes) {
+ free(blob);
+ free(path);
+ return false;
+ }
+
+ N3DSBcwav bcwav;
+ if (!N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav)) {
+ free(blob);
+ free(path);
+ return false;
+ }
+ if (!N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) {
+ free(blob);
+ free(path);
+ return false;
+ }
+
+ cachedSound->available = true;
+ cachedSound->headerAttempted = true;
+ cachedSound->headerAvailable = true;
+ cachedSound->blobOwned = true;
+ cachedSound->path = path;
+ cachedSound->blob = blob;
+ cachedSound->blobSize = blobSize;
+ cachedSound->bcwav = bcwav;
+ audio->cachedSoundBytes += blobSize;
+ return true;
+}
+
+static N3DSCachedSound* N3DSAudio_getCachedSoundIfAvailable(N3DSAudioSystem* audio, int32_t soundIndex) {
+ if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return NULL;
+ return audio->cachedSounds[soundIndex].available ? &audio->cachedSounds[soundIndex] : NULL;
+}
+
+static const char* N3DSAudio_getResolvedCachedSoundPath(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound) {
+ if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return NULL;
+
+ N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex];
+ if (cachedSound->path != NULL) return cachedSound->path;
+ if (cachedSound->pathAttempted) return NULL;
+
+ cachedSound->pathAttempted = true;
+ cachedSound->path = N3DSAudio_resolveSoundPath(audio, sound, soundIndex);
+ if (cachedSound->path == NULL) {
+ cachedSound->path = N3DSAudio_tryResolveGeneratedSoundPath(audio, sound, soundIndex);
+ }
+ return cachedSound->path;
+}
+
+static bool N3DSAudio_getCachedSoundHeader(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound, N3DSBcwav* out) {
+ if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return false;
+
+ N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex];
+ if (cachedSound->available || cachedSound->headerAvailable) {
+ if (out != NULL) *out = cachedSound->bcwav;
+ return true;
+ }
+ if (cachedSound->headerAttempted) return false;
+
+ cachedSound->headerAttempted = true;
+ const uint8_t* packedBlob = NULL;
+ uint32_t packedBlobSize = 0;
+ const N3DSPackedSoundBankEntry* packedEntry = N3DSAudio_getPackedSoundEntry(audio, soundIndex);
+ if (!N3DSAudio_soundLooksLikeMusic(sound) &&
+ packedEntry != NULL &&
+ !N3DSAudio_packedSoundEntryIsPcm16(packedEntry) &&
+ N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize)) {
+ if (!N3DSAudio_parseBcwavBlob(packedBlob, packedBlobSize, &cachedSound->bcwav)) return false;
+ } else {
+ const char* path = N3DSAudio_getResolvedCachedSoundPath(audio, soundIndex, sound);
+ if (path == NULL) return false;
+ if (!N3DSAudio_parseBcwavFile(path, &cachedSound->bcwav)) return false;
+ }
+
+ cachedSound->headerAvailable = true;
+ if (out != NULL) *out = cachedSound->bcwav;
+ return true;
+}
+
+static void N3DSAudio_prewarmSoundCache(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget) {
+ if (audio == NULL || audio->cachedSounds == NULL || audio->cachedSoundCount == 0) return;
+ if (maxSounds == 0 || byteBudget == 0) return;
+
+ uint32_t loadedSounds = 0;
+ uint32_t consumedBytes = 0;
+ uint32_t scanned = 0;
+
+ while (scanned < audio->cachedSoundCount && loadedSounds < maxSounds && consumedBytes < byteBudget) {
+ uint32_t soundIndex = audio->cachedSoundPrewarmCursor;
+ audio->cachedSoundPrewarmCursor++;
+ if (audio->cachedSoundPrewarmCursor >= audio->cachedSoundCount) {
+ audio->cachedSoundPrewarmCursor = 0;
+ }
+ scanned++;
+
+ N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex];
+ if (cachedSound->attempted) continue;
+
+ uint32_t bytesBefore = audio->cachedSoundBytes;
+ if (N3DSAudio_tryCacheSoundBlob(audio, (int32_t) soundIndex)) {
+ loadedSounds++;
+ consumedBytes += audio->cachedSoundBytes - bytesBefore;
+ }
+ }
+}
+
+static void N3DSAudio_requestCachePrewarmLocked(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget) {
+ if (audio == NULL || audio->cachedSounds == NULL || audio->cachedSoundCount == 0) return;
+ if (maxSounds == 0 || byteBudget == 0) return;
+ if (!audio->workerEnabled) {
+ N3DSAudio_prewarmSoundCache(audio, maxSounds, byteBudget);
+ return;
+ }
+
+ if (audio->pendingPrewarmSoundCount < maxSounds) {
+ audio->pendingPrewarmSoundCount = maxSounds;
+ }
+ if (audio->pendingPrewarmByteBudget < byteBudget) {
+ audio->pendingPrewarmByteBudget = byteBudget;
+ }
+ if (N3DSAudio_hasActiveStreamPlaybackLocked(audio)) return;
+ LightEvent_Signal(&audio->workerEvent);
+}
+
+#if N3DS_EAGER_SFX_PRELOAD
+static void N3DSAudio_preloadSounds(N3DSAudioSystem* audio, DataWin* dataWin) {
+ if (audio == NULL || dataWin == NULL || dataWin->sond.count == 0) return;
+
+ audio->cachedSounds = safeCalloc(dataWin->sond.count, sizeof(N3DSCachedSound));
+ audio->cachedSoundCount = dataWin->sond.count;
+
+ uint32_t loadedCount = 0;
+ repeat(dataWin->sond.count, i) {
+ if (N3DSAudio_tryCacheSoundBlob(audio, (int32_t) i)) loadedCount++;
+ }
+
+ fprintf(
+ stderr,
+ "N3DSAudio: preloaded %lu/%lu likely-SFX blobs (%lu bytes, cap=%lu)\n",
+ (unsigned long) loadedCount,
+ (unsigned long) dataWin->sond.count,
+ (unsigned long) audio->cachedSoundBytes,
+ (unsigned long) audio->maxCachedSoundBytes
+ );
+}
+#endif
+
+static N3DSStreamEntry* N3DSAudio_getActiveStreamEntry(N3DSAudioSystem* audio, int32_t soundIndex) {
+ int32_t slot = N3DSAudio_streamSlotFromSoundIndex(soundIndex);
+ if (slot < 0 || !audio->streams[slot].active) return NULL;
+ return &audio->streams[slot];
+}
+
+static bool N3DSAudio_canUseNativeAdpcmPlayback(const N3DSBcwav* bcwav, bool loop) {
+#if N3DS_FORCE_PCM_BCWAV_PLAYBACK
+ (void) bcwav;
+ (void) loop;
+ return false;
+#else
+ if (bcwav == NULL || bcwav->sampleRate == 0 || bcwav->channelCount == 0 || bcwav->channelCount > 2) return false;
+ if (!loop) return true;
+ return !bcwav->loop ||
+ bcwav->loopEnd <= bcwav->loopStart ||
+ N3DSAudio_isFrameAlignedSample(bcwav->loopStart);
+#endif
+}
+
+static void N3DSAudio_cleanupPartialStreamResources(N3DSAudioSystem* audio, N3DSSoundInstance* inst) {
+ if (inst == NULL) return;
+
+ if (inst->streamFile != NULL) {
+ fclose(inst->streamFile);
+ inst->streamFile = NULL;
+ }
+ repeat(inst->channelCount, channelIndex) {
+ repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) {
+ if (inst->streamAdpcm[channelIndex][bufferIndex] != NULL) {
+ linearFree(inst->streamAdpcm[channelIndex][bufferIndex]);
+ inst->streamAdpcm[channelIndex][bufferIndex] = NULL;
+ }
+ }
+ }
+ repeat(N3DS_STREAM_BUFFER_COUNT, i) {
+ if (inst->streamPcm[i] != NULL) {
+ linearFree(inst->streamPcm[i]);
+ inst->streamPcm[i] = NULL;
+ }
+ }
+ if (inst->ownsStreamBlob && inst->streamBlob != NULL) {
+ if (inst->streamBlobLinear) linearFree((void*) inst->streamBlob);
+ else free((void*) inst->streamBlob);
+ }
+ inst->streamBlob = NULL;
+ inst->streamBlobSize = 0;
+ inst->ownsStreamBlob = false;
+ inst->streamBlobLinear = false;
+
+ N3DSAudio_releaseChannel(audio, inst->channelId);
+ N3DSAudio_releaseChannel(audio, inst->secondaryChannelId);
+ inst->channelId = -1;
+ inst->secondaryChannelId = -1;
+
+ inst->isStream = false;
+ inst->useNativeAdpcm = false;
+ inst->streamFinished = false;
+ inst->sampleRate = 0;
+ inst->sampleCount = 0;
+ inst->channelCount = 0;
+ inst->baseRate = 0.0f;
+ memset(&inst->bcwav, 0, sizeof(inst->bcwav));
+ N3DSAudio_resetInstanceWaveBufState(inst);
+}
+
+static bool N3DSAudio_startNativeAdpcmPlayback(
+ N3DSAudioSystem* audio,
+ N3DSSoundInstance* inst,
+ const uint8_t* blob,
+ uint32_t blobSize,
+ const N3DSBcwav* bcwav
+) {
+ if (audio == NULL || inst == NULL || blob == NULL || bcwav == NULL) return false;
+
+ inst->channelId = N3DSAudio_acquireChannel(audio);
+ if (inst->channelId < 0) return false;
+ if (bcwav->channelCount == 2) {
+ inst->secondaryChannelId = N3DSAudio_acquireChannel(audio);
+ if (inst->secondaryChannelId < 0) return false;
+ }
+
+ uint8_t* nativeBlob = N3DSAudio_cloneBlobToLinear(blob, blobSize);
+ if (nativeBlob == NULL) return false;
+
+ inst->useNativeAdpcm = true;
+ inst->streamBlob = nativeBlob;
+ inst->streamBlobSize = blobSize;
+ inst->ownsStreamBlob = true;
+ inst->streamBlobLinear = true;
+ inst->bcwav = *bcwav;
+ inst->sampleRate = bcwav->sampleRate;
+ inst->sampleCount = bcwav->sampleCount;
+ inst->channelCount = bcwav->channelCount;
+ inst->baseRate = (float) bcwav->sampleRate;
+ inst->playbackStartTick = svcGetSystemTick();
+ N3DSAudio_refreshNonStreamReleaseTick(inst);
+
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ return N3DSAudio_primeNativeAdpcm(inst);
+}
+
+static bool N3DSAudio_startOwnedLinearNativeAdpcmPlayback(
+ N3DSAudioSystem* audio,
+ N3DSSoundInstance* inst,
+ const uint8_t* linearBlob,
+ uint32_t blobSize,
+ const N3DSBcwav* bcwav
+) {
+ if (audio == NULL || inst == NULL || linearBlob == NULL || bcwav == NULL) return false;
+
+ inst->channelId = N3DSAudio_acquireChannel(audio);
+ if (inst->channelId < 0) return false;
+ if (bcwav->channelCount == 2) {
+ inst->secondaryChannelId = N3DSAudio_acquireChannel(audio);
+ if (inst->secondaryChannelId < 0) return false;
+ }
+
+ inst->useNativeAdpcm = true;
+ inst->streamBlob = linearBlob;
+ inst->streamBlobSize = blobSize;
+ inst->ownsStreamBlob = true;
+ inst->streamBlobLinear = true;
+ inst->bcwav = *bcwav;
+ inst->sampleRate = bcwav->sampleRate;
+ inst->sampleCount = bcwav->sampleCount;
+ inst->channelCount = bcwav->channelCount;
+ inst->baseRate = (float) bcwav->sampleRate;
+ inst->playbackStartTick = svcGetSystemTick();
+ N3DSAudio_refreshNonStreamReleaseTick(inst);
+
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ return N3DSAudio_primeNativeAdpcm(inst);
+}
+
+static bool N3DSAudio_startOwnedPcmPlayback(
+ N3DSAudioSystem* audio,
+ N3DSSoundInstance* inst,
+ int16_t* pcm,
+ uint32_t sampleRate,
+ uint32_t sampleCount,
+ uint8_t channelCount,
+ bool loop
+) {
+ if (audio == NULL || inst == NULL || pcm == NULL) return false;
+ if (sampleRate == 0 || sampleCount == 0 || channelCount == 0 || channelCount > 2) return false;
+ if (!loop) N3DSAudio_softenPcmTail(pcm, sampleCount, channelCount);
+
+ inst->channelId = N3DSAudio_acquireChannel(audio);
+ if (inst->channelId < 0) return false;
+ ndspChnSetInterp(inst->channelId, NDSP_INTERP_LINEAR);
+
+ inst->pcmData = pcm;
+ inst->sampleRate = sampleRate;
+ inst->sampleCount = sampleCount;
+ inst->channelCount = channelCount;
+ inst->baseRate = (float) sampleRate;
+ inst->playbackStartTick = svcGetSystemTick();
+ N3DSAudio_refreshNonStreamReleaseTick(inst);
+ N3DSAudio_resetInstanceWaveBufState(inst);
+ inst->waveBufs[0].data_pcm16 = pcm;
+ inst->waveBufs[0].nsamples = sampleCount;
+ inst->waveBufs[0].looping = loop;
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[0]);
+ return true;
+}
+
+static bool N3DSAudio_startSharedNativeAdpcmPlayback(
+ N3DSAudioSystem* audio,
+ N3DSSoundInstance* inst,
+ const uint8_t* linearBlob,
+ uint32_t blobSize,
+ const N3DSBcwav* bcwav
+) {
+ if (audio == NULL || inst == NULL || linearBlob == NULL || bcwav == NULL) return false;
+
+ inst->channelId = N3DSAudio_acquireChannel(audio);
+ if (inst->channelId < 0) return false;
+ if (bcwav->channelCount == 2) {
+ inst->secondaryChannelId = N3DSAudio_acquireChannel(audio);
+ if (inst->secondaryChannelId < 0) return false;
+ }
+
+ inst->useNativeAdpcm = true;
+ inst->streamBlob = linearBlob;
+ inst->streamBlobSize = blobSize;
+ inst->ownsStreamBlob = false;
+ inst->streamBlobLinear = true;
+ inst->bcwav = *bcwav;
+ inst->sampleRate = bcwav->sampleRate;
+ inst->sampleCount = bcwav->sampleCount;
+ inst->channelCount = bcwav->channelCount;
+ inst->baseRate = (float) bcwav->sampleRate;
+ inst->playbackStartTick = svcGetSystemTick();
+ N3DSAudio_refreshNonStreamReleaseTick(inst);
+
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ return N3DSAudio_primeNativeAdpcm(inst);
+}
+
+static bool N3DSAudio_primeNativeAdpcm(N3DSSoundInstance* inst) {
+ if (!inst->useNativeAdpcm || inst->streamBlob == NULL) return false;
+
+ uint32_t loopStart = 0;
+ uint32_t loopEnd = inst->bcwav.sampleCount;
+ bool useLoopRegion = inst->loop &&
+ inst->bcwav.loop &&
+ inst->bcwav.loopStart > 0 &&
+ inst->bcwav.loopEnd > inst->bcwav.loopStart;
+ if (useLoopRegion) {
+ loopStart = inst->bcwav.loopStart;
+ loopEnd = inst->bcwav.loopEnd;
+ }
+
+ N3DSAudio_resetInstanceWaveBufState(inst);
+ inst->bufferStartSample[0] = 0;
+ inst->bufferStartSample[1] = loopStart;
+
+ inst->adpcmStates[0].index = inst->bcwav.channels[0].startContext.predictorScale;
+ inst->adpcmStates[0].history0 = inst->bcwav.channels[0].startContext.yn1;
+ inst->adpcmStates[0].history1 = inst->bcwav.channels[0].startContext.yn2;
+
+ inst->waveBufs[0].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[0].dataOffset;
+ inst->waveBufs[0].nsamples = useLoopRegion ? loopStart : inst->bcwav.sampleCount;
+ inst->waveBufs[0].adpcm_data = &inst->adpcmStates[0];
+ inst->waveBufs[0].looping = inst->loop && !useLoopRegion;
+
+ if (inst->secondaryChannelId >= 0) {
+ inst->adpcmStates[1].index = inst->bcwav.channels[1].startContext.predictorScale;
+ inst->adpcmStates[1].history0 = inst->bcwav.channels[1].startContext.yn1;
+ inst->adpcmStates[1].history1 = inst->bcwav.channels[1].startContext.yn2;
+
+ inst->secondaryWaveBufs[0].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[1].dataOffset;
+ inst->secondaryWaveBufs[0].nsamples = inst->waveBufs[0].nsamples;
+ inst->secondaryWaveBufs[0].adpcm_data = &inst->adpcmStates[1];
+ inst->secondaryWaveBufs[0].looping = inst->waveBufs[0].looping;
+ }
+
+ if (useLoopRegion) {
+ inst->adpcmStates[2].index = inst->bcwav.channels[0].loopContext.predictorScale;
+ inst->adpcmStates[2].history0 = inst->bcwav.channels[0].loopContext.yn1;
+ inst->adpcmStates[2].history1 = inst->bcwav.channels[0].loopContext.yn2;
+
+ inst->waveBufs[1].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[0].dataOffset + N3DSAudio_adpcmByteOffsetForSample(loopStart);
+ inst->waveBufs[1].nsamples = loopEnd - loopStart;
+ inst->waveBufs[1].adpcm_data = &inst->adpcmStates[2];
+ inst->waveBufs[1].looping = true;
+
+ if (inst->secondaryChannelId >= 0) {
+ inst->adpcmStates[3].index = inst->bcwav.channels[1].loopContext.predictorScale;
+ inst->adpcmStates[3].history0 = inst->bcwav.channels[1].loopContext.yn1;
+ inst->adpcmStates[3].history1 = inst->bcwav.channels[1].loopContext.yn2;
+
+ inst->secondaryWaveBufs[1].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[1].dataOffset + N3DSAudio_adpcmByteOffsetForSample(loopStart);
+ inst->secondaryWaveBufs[1].nsamples = inst->waveBufs[1].nsamples;
+ inst->secondaryWaveBufs[1].adpcm_data = &inst->adpcmStates[3];
+ inst->secondaryWaveBufs[1].looping = true;
+ }
+ }
+
+ DSP_FlushDataCache((void*) inst->streamBlob, inst->streamBlobSize);
+ ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[0]);
+ if (inst->secondaryChannelId >= 0) {
+ ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[0]);
+ }
+
+ if (useLoopRegion) {
+ ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[1]);
+ if (inst->secondaryChannelId >= 0) {
+ ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[1]);
+ }
+ }
+ return true;
+}
+
+static void N3DSAudio_releaseInstance(N3DSAudioSystem* audio, N3DSSoundInstance* inst) {
+ if (!inst->active) return;
+ if (inst->isStream) {
+ N3DSDebugLog_event(
+ "audio_stop",
+ "instance=%ld sound=%ld native=%d sample=%lu/%lu ch=%ld/%ld",
+ (long) inst->instanceId,
+ (long) inst->soundIndex,
+ inst->useNativeAdpcm ? 1 : 0,
+ (unsigned long) inst->currentSample,
+ (unsigned long) inst->sampleCount,
+ (long) inst->channelId,
+ (long) inst->secondaryChannelId
+ );
+ }
+ if (audio != NULL && inst->isStream) {
+ N3DSAudio_cancelAsyncFillLocked(audio, inst);
+ }
+
+ N3DSAudio_releaseChannel(audio, inst->channelId);
+ N3DSAudio_releaseChannel(audio, inst->secondaryChannelId);
+
+ if (inst->pcmData != NULL) linearFree(inst->pcmData);
+ repeat(inst->channelCount, channelIndex) {
+ repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) {
+ if (inst->streamAdpcm[channelIndex][bufferIndex] != NULL) {
+ linearFree(inst->streamAdpcm[channelIndex][bufferIndex]);
+ }
+ }
+ }
+ repeat(N3DS_STREAM_BUFFER_COUNT, i) {
+ if (inst->streamPcm[i] != NULL) linearFree(inst->streamPcm[i]);
+ }
+ if (inst->streamFile != NULL) fclose(inst->streamFile);
+ if (inst->ownsStreamBlob) {
+ if (inst->streamBlobLinear) linearFree((void*) inst->streamBlob);
+ else free((void*) inst->streamBlob);
+ }
+
+ memset(inst, 0, sizeof(*inst));
+}
+
+static N3DSSoundInstance* N3DSAudio_findFreeInstanceInRange(N3DSAudioSystem* audio, int32_t start, int32_t endExclusive) {
+ u64 nowTick = svcGetSystemTick();
+ for (int32_t i = start; i < endExclusive; ++i) {
+ if (!audio->instances[i].active) return &audio->instances[i];
+ }
+ for (int32_t i = start; i < endExclusive; ++i) {
+ bool finishedPcm = N3DSAudio_nonStreamPlaybackFinished(&audio->instances[i], nowTick);
+ if (finishedPcm) {
+ N3DSAudio_releaseInstance(audio, &audio->instances[i]);
+ return &audio->instances[i];
+ }
+ }
+ return NULL;
+}
+
+static N3DSSoundInstance* N3DSAudio_findFreeInstance(N3DSAudioSystem* audio, bool preferStreamSlots) {
+ int32_t split = N3DS_MAX_SOUND_INSTANCES - N3DS_RESERVED_STREAM_INSTANCES;
+ if (split < 0) split = 0;
+ if (split > N3DS_MAX_SOUND_INSTANCES) split = N3DS_MAX_SOUND_INSTANCES;
+
+ if (preferStreamSlots) {
+ N3DSSoundInstance* inst = N3DSAudio_findFreeInstanceInRange(audio, split, N3DS_MAX_SOUND_INSTANCES);
+ if (inst != NULL) return inst;
+ return N3DSAudio_findFreeInstanceInRange(audio, 0, split);
+ }
+
+ N3DSSoundInstance* inst = N3DSAudio_findFreeInstanceInRange(audio, 0, split);
+ if (inst != NULL) return inst;
+ return N3DSAudio_findFreeInstanceInRange(audio, split, N3DS_MAX_SOUND_INSTANCES);
+}
+
+static void N3DSAudio_streamResetDecoder(N3DSSoundInstance* inst) {
+ inst->currentSample = 0;
+ inst->pendingSamples = 0;
+ inst->pendingOffset = 0;
+ repeat(inst->bcwav.channelCount, i) {
+ inst->decode[i].nextFrameIndex = 0;
+ inst->decode[i].predictorScale = inst->bcwav.channels[i].startContext.predictorScale;
+ inst->decode[i].hist1 = inst->bcwav.channels[i].startContext.yn1;
+ inst->decode[i].hist2 = inst->bcwav.channels[i].startContext.yn2;
+ inst->fileCacheFrameBase[i] = 0;
+ inst->fileCacheFrameCount[i] = 0;
+ }
+}
+
+static bool N3DSAudio_streamEnsureFileCache(N3DSSoundInstance* inst, uint32_t channelIndex) {
+ if (inst == NULL || inst->streamFile == NULL || channelIndex >= inst->bcwav.channelCount) return false;
+
+ N3DSStreamDecodeState* state = &inst->decode[channelIndex];
+ uint32_t frameIndex = state->nextFrameIndex;
+ uint32_t cacheBase = inst->fileCacheFrameBase[channelIndex];
+ uint32_t cacheCount = inst->fileCacheFrameCount[channelIndex];
+ if (frameIndex >= cacheBase && frameIndex < cacheBase + cacheCount) return true;
+
+ uint32_t totalFrames = (inst->sampleCount + 13u) / 14u;
+ if (frameIndex >= totalFrames) return false;
+
+ uint32_t framesToRead = totalFrames - frameIndex;
+ if (framesToRead > N3DS_STREAM_ADPCM_CACHE_FRAMES) framesToRead = N3DS_STREAM_ADPCM_CACHE_FRAMES;
+
+ uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + frameIndex * 8u;
+ uint32_t byteCount = framesToRead * 8u;
+ if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false;
+ if (fread(inst->fileCache[channelIndex], 1, byteCount, inst->streamFile) != byteCount) return false;
+
+ inst->fileCacheFrameBase[channelIndex] = frameIndex;
+ inst->fileCacheFrameCount[channelIndex] = framesToRead;
+ return true;
+}
+
+static bool N3DSAudio_streamDecodeNextFrame(N3DSSoundInstance* inst, int16_t outPcm[14 * 2], uint32_t* outSamples) {
+ if (inst->currentSample >= inst->sampleCount) {
+ *outSamples = 0;
+ return true;
+ }
+
+ int16_t monoSamples[2][14];
+ repeat(inst->bcwav.channelCount, channelIndex) {
+ N3DSStreamDecodeState* state = &inst->decode[channelIndex];
+ uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + state->nextFrameIndex * 8u;
+ const uint8_t* frame = NULL;
+ uint8_t frameBuffer[8];
+ if (inst->streamBlob != NULL) {
+ if (byteOffset + 8u > inst->streamBlobSize) return false;
+ frame = inst->streamBlob + byteOffset;
+ } else {
+ if (!N3DSAudio_streamEnsureFileCache(inst, channelIndex)) return false;
+ uint32_t cacheIndex = state->nextFrameIndex - inst->fileCacheFrameBase[channelIndex];
+ if (cacheIndex >= inst->fileCacheFrameCount[channelIndex]) return false;
+ memcpy(frameBuffer, inst->fileCache[channelIndex] + cacheIndex * 8u, sizeof(frameBuffer));
+ frame = frameBuffer;
+ }
+ state->predictorScale = frame[0];
+ N3DSAudio_decodeFrame(&inst->bcwav.channels[channelIndex], frame, &state->hist1, &state->hist2, monoSamples[channelIndex]);
+ state->nextFrameIndex++;
+ }
+
+ uint32_t samplesThisFrame = inst->sampleCount - inst->currentSample;
+ if (samplesThisFrame > 14u) samplesThisFrame = 14u;
+ repeat(samplesThisFrame, sampleIndex) {
+ if (inst->bcwav.channelCount == 1) {
+ outPcm[sampleIndex] = monoSamples[0][sampleIndex];
+ } else {
+ outPcm[sampleIndex * 2 + 0] = monoSamples[0][sampleIndex];
+ outPcm[sampleIndex * 2 + 1] = monoSamples[1][sampleIndex];
+ }
+ }
+ inst->currentSample += samplesThisFrame;
+ *outSamples = samplesThisFrame;
+ return true;
+}
+
+static bool N3DSAudio_streamSeekSamples(N3DSSoundInstance* inst, uint32_t targetSample) {
+ if (targetSample > inst->sampleCount) targetSample = inst->sampleCount;
+
+ N3DSAudio_streamResetDecoder(inst);
+ if (targetSample == 0) return true;
+ if (inst->bcwav.loop &&
+ targetSample == inst->bcwav.loopStart &&
+ N3DSAudio_isFrameAlignedSample(targetSample)) {
+ uint32_t frameIndex = targetSample / 14u;
+ repeat(inst->bcwav.channelCount, i) {
+ inst->decode[i].nextFrameIndex = frameIndex;
+ inst->decode[i].predictorScale = inst->bcwav.channels[i].loopContext.predictorScale;
+ inst->decode[i].hist1 = inst->bcwav.channels[i].loopContext.yn1;
+ inst->decode[i].hist2 = inst->bcwav.channels[i].loopContext.yn2;
+ }
+ inst->currentSample = targetSample;
+ return true;
+ }
+
+ int16_t framePcm[14 * 2];
+ uint32_t frameSamples = 0;
+ while (inst->currentSample + 14u <= targetSample) {
+ if (!N3DSAudio_streamDecodeNextFrame(inst, framePcm, &frameSamples)) return false;
+ if (frameSamples == 0) return true;
+ }
+
+ if (inst->currentSample < targetSample) {
+ uint32_t baseSample = inst->currentSample;
+ if (!N3DSAudio_streamDecodeNextFrame(inst, framePcm, &frameSamples)) return false;
+ uint32_t skip = targetSample - baseSample;
+ if (skip < frameSamples) {
+ uint32_t remaining = frameSamples - skip;
+ size_t stride = inst->bcwav.channelCount;
+ memcpy(inst->pendingPcm, framePcm + skip * stride, remaining * stride * sizeof(int16_t));
+ inst->pendingSamples = remaining;
+ inst->pendingOffset = 0;
+ }
+ inst->currentSample = targetSample;
+ }
+
+ return true;
+}
+
+static bool N3DSAudio_streamEnsureFileCacheAsync(N3DSSoundInstance* inst, N3DSStreamFillCursor* cursor, uint32_t channelIndex) {
+ if (inst == NULL || cursor == NULL || inst->streamFile == NULL || channelIndex >= inst->bcwav.channelCount) return false;
+
+ N3DSStreamDecodeState* state = &cursor->decode[channelIndex];
+ uint32_t frameIndex = state->nextFrameIndex;
+ uint32_t cacheBase = cursor->fileCacheFrameBase[channelIndex];
+ uint32_t cacheCount = cursor->fileCacheFrameCount[channelIndex];
+ if (frameIndex >= cacheBase && frameIndex < cacheBase + cacheCount) return true;
+
+ uint32_t totalFrames = (inst->sampleCount + 13u) / 14u;
+ if (frameIndex >= totalFrames) return false;
+
+ uint32_t framesToRead = totalFrames - frameIndex;
+ if (framesToRead > N3DS_STREAM_ADPCM_CACHE_FRAMES) framesToRead = N3DS_STREAM_ADPCM_CACHE_FRAMES;
+
+ uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + frameIndex * 8u;
+ uint32_t byteCount = framesToRead * 8u;
+ if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false;
+ if (fread(cursor->fileCache[channelIndex], 1, byteCount, inst->streamFile) != byteCount) return false;
+
+ cursor->fileCacheFrameBase[channelIndex] = frameIndex;
+ cursor->fileCacheFrameCount[channelIndex] = framesToRead;
+ return true;
+}
+
+static bool N3DSAudio_streamDecodeNextFrameAsync(
+ N3DSSoundInstance* inst,
+ N3DSStreamFillCursor* cursor,
+ int16_t outPcm[14 * 2],
+ uint32_t* outSamples
+) {
+ if (inst == NULL || cursor == NULL || outPcm == NULL || outSamples == NULL) return false;
+ if (cursor->currentSample >= inst->sampleCount) {
+ *outSamples = 0;
+ return true;
+ }
+
+ int16_t monoSamples[2][14];
+ repeat(inst->bcwav.channelCount, channelIndex) {
+ N3DSStreamDecodeState* state = &cursor->decode[channelIndex];
+ uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + state->nextFrameIndex * 8u;
+ const uint8_t* frame = NULL;
+ uint8_t frameBuffer[8];
+ if (inst->streamBlob != NULL) {
+ if (byteOffset + 8u > inst->streamBlobSize) return false;
+ frame = inst->streamBlob + byteOffset;
+ } else {
+ if (!N3DSAudio_streamEnsureFileCacheAsync(inst, cursor, channelIndex)) return false;
+ uint32_t cacheIndex = state->nextFrameIndex - cursor->fileCacheFrameBase[channelIndex];
+ if (cacheIndex >= cursor->fileCacheFrameCount[channelIndex]) return false;
+ memcpy(frameBuffer, cursor->fileCache[channelIndex] + cacheIndex * 8u, sizeof(frameBuffer));
+ frame = frameBuffer;
+ }
+ state->predictorScale = frame[0];
+ N3DSAudio_decodeFrame(&inst->bcwav.channels[channelIndex], frame, &state->hist1, &state->hist2, monoSamples[channelIndex]);
+ state->nextFrameIndex++;
+ }
+
+ uint32_t samplesThisFrame = inst->sampleCount - cursor->currentSample;
+ if (samplesThisFrame > 14u) samplesThisFrame = 14u;
+ repeat(samplesThisFrame, sampleIndex) {
+ if (inst->bcwav.channelCount == 1) {
+ outPcm[sampleIndex] = monoSamples[0][sampleIndex];
+ } else {
+ outPcm[sampleIndex * 2 + 0] = monoSamples[0][sampleIndex];
+ outPcm[sampleIndex * 2 + 1] = monoSamples[1][sampleIndex];
+ }
+ }
+ cursor->currentSample += samplesThisFrame;
+ *outSamples = samplesThisFrame;
+ return true;
+}
+
+static bool N3DSAudio_streamSeekSamplesAsync(N3DSSoundInstance* inst, N3DSStreamFillCursor* cursor, uint32_t targetSample) {
+ if (inst == NULL || cursor == NULL) return false;
+ if (targetSample > inst->sampleCount) targetSample = inst->sampleCount;
+
+ cursor->currentSample = 0;
+ cursor->pendingSamples = 0;
+ cursor->pendingOffset = 0;
+ repeat(inst->bcwav.channelCount, i) {
+ cursor->decode[i].nextFrameIndex = 0;
+ cursor->decode[i].predictorScale = inst->bcwav.channels[i].startContext.predictorScale;
+ cursor->decode[i].hist1 = inst->bcwav.channels[i].startContext.yn1;
+ cursor->decode[i].hist2 = inst->bcwav.channels[i].startContext.yn2;
+ cursor->fileCacheFrameBase[i] = 0;
+ cursor->fileCacheFrameCount[i] = 0;
+ }
+ if (targetSample == 0) return true;
+ if (inst->bcwav.loop &&
+ targetSample == inst->bcwav.loopStart &&
+ N3DSAudio_isFrameAlignedSample(targetSample)) {
+ uint32_t frameIndex = targetSample / 14u;
+ repeat(inst->bcwav.channelCount, i) {
+ cursor->decode[i].nextFrameIndex = frameIndex;
+ cursor->decode[i].predictorScale = inst->bcwav.channels[i].loopContext.predictorScale;
+ cursor->decode[i].hist1 = inst->bcwav.channels[i].loopContext.yn1;
+ cursor->decode[i].hist2 = inst->bcwav.channels[i].loopContext.yn2;
+ }
+ cursor->currentSample = targetSample;
+ return true;
+ }
+
+ int16_t framePcm[14 * 2];
+ uint32_t frameSamples = 0;
+ while (cursor->currentSample + 14u <= targetSample) {
+ if (!N3DSAudio_streamDecodeNextFrameAsync(inst, cursor, framePcm, &frameSamples)) return false;
+ if (frameSamples == 0) return true;
+ }
+
+ if (cursor->currentSample < targetSample) {
+ uint32_t baseSample = cursor->currentSample;
+ if (!N3DSAudio_streamDecodeNextFrameAsync(inst, cursor, framePcm, &frameSamples)) return false;
+ uint32_t skip = targetSample - baseSample;
+ if (skip < frameSamples) {
+ uint32_t remaining = frameSamples - skip;
+ size_t stride = inst->bcwav.channelCount;
+ memcpy(cursor->pendingPcm, framePcm + skip * stride, remaining * stride * sizeof(int16_t));
+ cursor->pendingSamples = remaining;
+ cursor->pendingOffset = 0;
+ }
+ cursor->currentSample = targetSample;
+ }
+
+ return true;
+}
+
+static bool N3DSAudio_fillStreamWaveBufAsync(
+ N3DSSoundInstance* inst,
+ int bufferIndex,
+ N3DSStreamFillCursor* cursor,
+ uint32_t* outStartSample,
+ uint32_t* outProducedSamples
+) {
+ if (inst == NULL || cursor == NULL || outStartSample == NULL || outProducedSamples == NULL) return false;
+
+ size_t stride = inst->bcwav.channelCount;
+ int16_t* out = inst->streamPcm[bufferIndex];
+ uint32_t produced = 0;
+ uint32_t startSample = cursor->currentSample;
+
+ while (produced < N3DS_STREAM_CHUNK_SAMPLES) {
+ if (cursor->pendingOffset < cursor->pendingSamples) {
+ uint32_t take = cursor->pendingSamples - cursor->pendingOffset;
+ if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced;
+ memcpy(
+ out + produced * stride,
+ cursor->pendingPcm + cursor->pendingOffset * stride,
+ take * stride * sizeof(int16_t)
+ );
+ cursor->pendingOffset += take;
+ produced += take;
+ cursor->currentSample += take;
+ if (cursor->pendingOffset >= cursor->pendingSamples) {
+ cursor->pendingSamples = 0;
+ cursor->pendingOffset = 0;
+ }
+ continue;
+ }
+
+ if (cursor->currentSample >= inst->sampleCount) {
+ if (inst->loop) {
+ if (produced > 0) break;
+ if (!N3DSAudio_streamSeekSamplesAsync(inst, cursor, inst->bcwav.loopStart)) return false;
+ startSample = cursor->currentSample;
+ continue;
+ }
+ break;
+ }
+
+ int16_t framePcm[14 * 2];
+ uint32_t frameSamples = 0;
+ if (!N3DSAudio_streamDecodeNextFrameAsync(inst, cursor, framePcm, &frameSamples)) return false;
+ if (frameSamples == 0) break;
+
+ uint32_t take = frameSamples;
+ if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced;
+ memcpy(out + produced * stride, framePcm, take * stride * sizeof(int16_t));
+ produced += take;
+ }
+
+ if (produced == 0) return false;
+
+ DSP_FlushDataCache(out, produced * stride * sizeof(int16_t));
+ *outStartSample = startSample;
+ *outProducedSamples = produced;
+ return true;
+}
+
+static bool N3DSAudio_fillNativeStreamWaveBufAsync(
+ N3DSSoundInstance* inst,
+ int bufferIndex,
+ N3DSStreamFillCursor* cursor,
+ uint32_t* outStartSample,
+ uint32_t* outProducedSamples,
+ ndspAdpcmData outAdpcmStates[2]
+) {
+ if (inst == NULL || cursor == NULL || outStartSample == NULL || outProducedSamples == NULL || outAdpcmStates == NULL) return false;
+ if (inst->streamFile == NULL || inst->channelCount == 0) return false;
+
+ uint32_t startSample = cursor->currentSample;
+ if (startSample >= inst->sampleCount) {
+ if (!inst->loop) return false;
+ if (!N3DSAudio_isFrameAlignedSample(inst->bcwav.loopStart)) return false;
+ if (!N3DSAudio_streamSeekSamplesAsync(inst, cursor, inst->bcwav.loopStart)) return false;
+ startSample = cursor->currentSample;
+ }
+
+ uint32_t endSample = startSample + N3DS_STREAM_CHUNK_SAMPLES;
+ if (endSample > inst->sampleCount) endSample = inst->sampleCount;
+ if (inst->loop && inst->bcwav.loop && inst->bcwav.loopEnd > inst->bcwav.loopStart && endSample > inst->bcwav.loopEnd) {
+ endSample = inst->bcwav.loopEnd;
+ }
+ uint32_t sampleCount = endSample - startSample;
+ if (sampleCount == 0) return false;
+
+ repeat(inst->channelCount, channelIndex) {
+ uint32_t startFrame = startSample / 14u;
+ uint32_t frameCount = (sampleCount + 13u) / 14u;
+ uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + startFrame * 8u;
+ uint32_t byteCount = frameCount * 8u;
+ if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false;
+ if (fread(inst->streamAdpcm[channelIndex][bufferIndex], 1, byteCount, inst->streamFile) != byteCount) return false;
+ DSP_FlushDataCache(inst->streamAdpcm[channelIndex][bufferIndex], byteCount);
+ }
+
+ N3DSDspContext primaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[0], &cursor->decode[0], startSample);
+ outAdpcmStates[0].index = primaryContext.predictorScale;
+ outAdpcmStates[0].history0 = primaryContext.yn1;
+ outAdpcmStates[0].history1 = primaryContext.yn2;
+
+ if (inst->secondaryChannelId >= 0) {
+ N3DSDspContext secondaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[1], &cursor->decode[1], startSample);
+ outAdpcmStates[1].index = secondaryContext.predictorScale;
+ outAdpcmStates[1].history0 = secondaryContext.yn1;
+ outAdpcmStates[1].history1 = secondaryContext.yn2;
+ } else {
+ memset(&outAdpcmStates[1], 0, sizeof(outAdpcmStates[1]));
+ }
+
+ bool atLoopBoundary = inst->loop &&
+ inst->bcwav.loop &&
+ inst->bcwav.loopEnd > inst->bcwav.loopStart &&
+ endSample >= inst->bcwav.loopEnd;
+
+ {
+ N3DSStreamDecodeState nextDecode[2];
+ memcpy(nextDecode, cursor->decode, sizeof(nextDecode));
+
+ uint32_t framesToAdvance = (sampleCount + 13u) / 14u;
+ repeat(inst->channelCount, channelIndex) {
+ uint32_t channelSample = startSample;
+ repeat(framesToAdvance, frameIndex) {
+ const uint8_t* frame = inst->streamAdpcm[channelIndex][bufferIndex] + frameIndex * 8u;
+ int16_t scratch[14];
+ nextDecode[channelIndex].predictorScale = frame[0];
+ N3DSAudio_decodeFrame(
+ &inst->bcwav.channels[channelIndex],
+ frame,
+ &nextDecode[channelIndex].hist1,
+ &nextDecode[channelIndex].hist2,
+ scratch
+ );
+ nextDecode[channelIndex].nextFrameIndex++;
+
+ uint32_t remaining = endSample - channelSample;
+ uint32_t frameSamples = remaining > 14u ? 14u : remaining;
+ channelSample += frameSamples;
+ }
+ }
+
+ if (atLoopBoundary) {
+ uint32_t loopFrame = inst->bcwav.loopStart / 14u;
+ repeat(inst->channelCount, channelIndex) {
+ cursor->decode[channelIndex].nextFrameIndex = loopFrame;
+ cursor->decode[channelIndex].predictorScale = inst->bcwav.channels[channelIndex].loopContext.predictorScale;
+ cursor->decode[channelIndex].hist1 = inst->bcwav.channels[channelIndex].loopContext.yn1;
+ cursor->decode[channelIndex].hist2 = inst->bcwav.channels[channelIndex].loopContext.yn2;
+ }
+ cursor->currentSample = inst->bcwav.loopStart;
+ } else {
+ memcpy(cursor->decode, nextDecode, sizeof(nextDecode));
+ cursor->currentSample = endSample;
+ }
+ }
+
+ *outStartSample = startSample;
+ *outProducedSamples = sampleCount;
+ return true;
+}
+
+static bool N3DSAudio_fillStreamWaveBuf(N3DSSoundInstance* inst, int bufferIndex) {
+ size_t stride = inst->bcwav.channelCount;
+ int16_t* out = inst->streamPcm[bufferIndex];
+ uint32_t produced = 0;
+ uint32_t startSample = inst->currentSample;
+
+ while (produced < N3DS_STREAM_CHUNK_SAMPLES) {
+ if (inst->pendingOffset < inst->pendingSamples) {
+ uint32_t take = inst->pendingSamples - inst->pendingOffset;
+ if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced;
+ memcpy(
+ out + produced * stride,
+ inst->pendingPcm + inst->pendingOffset * stride,
+ take * stride * sizeof(int16_t)
+ );
+ inst->pendingOffset += take;
+ produced += take;
+ inst->currentSample += take;
+ if (inst->pendingOffset >= inst->pendingSamples) {
+ inst->pendingSamples = 0;
+ inst->pendingOffset = 0;
+ }
+ continue;
+ }
+
+ if (inst->currentSample >= inst->sampleCount) {
+ if (inst->loop) {
+ if (produced > 0) break;
+ if (!N3DSAudio_streamSeekSamples(inst, inst->bcwav.loopStart)) return false;
+ startSample = inst->currentSample;
+ continue;
+ }
+ break;
+ }
+
+ int16_t framePcm[14 * 2];
+ uint32_t frameSamples = 0;
+ if (!N3DSAudio_streamDecodeNextFrame(inst, framePcm, &frameSamples)) return false;
+ if (frameSamples == 0) break;
+
+ uint32_t take = frameSamples;
+ if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced;
+ memcpy(out + produced * stride, framePcm, take * stride * sizeof(int16_t));
+ produced += take;
+ }
+
+ if (produced == 0) return false;
+
+ memset(&inst->waveBufs[bufferIndex], 0, sizeof(inst->waveBufs[bufferIndex]));
+ inst->waveBufs[bufferIndex].data_pcm16 = out;
+ inst->waveBufs[bufferIndex].nsamples = produced;
+ inst->waveBufs[bufferIndex].looping = false;
+ inst->bufferStartSample[bufferIndex] = startSample;
+ DSP_FlushDataCache(out, produced * stride * sizeof(int16_t));
+ return true;
+}
+
+static bool N3DSAudio_fillNativeStreamWaveBuf(N3DSSoundInstance* inst, int bufferIndex) {
+ if (inst == NULL || inst->streamFile == NULL || inst->channelCount == 0) return false;
+
+ uint32_t startSample = inst->currentSample;
+ if (startSample >= inst->sampleCount) {
+ if (!inst->loop) return false;
+ if (!N3DSAudio_isFrameAlignedSample(inst->bcwav.loopStart)) return false;
+ if (!N3DSAudio_streamSeekSamples(inst, inst->bcwav.loopStart)) return false;
+ startSample = inst->currentSample;
+ }
+
+ uint32_t endSample = startSample + N3DS_STREAM_CHUNK_SAMPLES;
+ if (endSample > inst->sampleCount) endSample = inst->sampleCount;
+ if (inst->loop && inst->bcwav.loop && inst->bcwav.loopEnd > inst->bcwav.loopStart && endSample > inst->bcwav.loopEnd) {
+ endSample = inst->bcwav.loopEnd;
+ }
+ uint32_t sampleCount = endSample - startSample;
+ if (sampleCount == 0) return false;
+
+ repeat(inst->channelCount, channelIndex) {
+ uint32_t startFrame = startSample / 14u;
+ uint32_t frameCount = (sampleCount + 13u) / 14u;
+ uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + startFrame * 8u;
+ uint32_t byteCount = frameCount * 8u;
+ if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false;
+ if (fread(inst->streamAdpcm[channelIndex][bufferIndex], 1, byteCount, inst->streamFile) != byteCount) return false;
+ DSP_FlushDataCache(inst->streamAdpcm[channelIndex][bufferIndex], byteCount);
+ }
+
+ ndspAdpcmData* primaryState = &inst->adpcmStates[bufferIndex * 2];
+ N3DSDspContext primaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[0], &inst->decode[0], startSample);
+ primaryState->index = primaryContext.predictorScale;
+ primaryState->history0 = primaryContext.yn1;
+ primaryState->history1 = primaryContext.yn2;
+
+ memset(&inst->waveBufs[bufferIndex], 0, sizeof(inst->waveBufs[bufferIndex]));
+ inst->waveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[0][bufferIndex];
+ inst->waveBufs[bufferIndex].nsamples = sampleCount;
+ inst->waveBufs[bufferIndex].adpcm_data = primaryState;
+ inst->waveBufs[bufferIndex].looping = false;
+ inst->bufferStartSample[bufferIndex] = startSample;
+
+ if (inst->secondaryChannelId >= 0) {
+ ndspAdpcmData* secondaryState = &inst->adpcmStates[bufferIndex * 2 + 1];
+ N3DSDspContext secondaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[1], &inst->decode[1], startSample);
+ secondaryState->index = secondaryContext.predictorScale;
+ secondaryState->history0 = secondaryContext.yn1;
+ secondaryState->history1 = secondaryContext.yn2;
+
+ memset(&inst->secondaryWaveBufs[bufferIndex], 0, sizeof(inst->secondaryWaveBufs[bufferIndex]));
+ inst->secondaryWaveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[1][bufferIndex];
+ inst->secondaryWaveBufs[bufferIndex].nsamples = sampleCount;
+ inst->secondaryWaveBufs[bufferIndex].adpcm_data = secondaryState;
+ inst->secondaryWaveBufs[bufferIndex].looping = false;
+ }
+
+ bool atLoopBoundary = inst->loop &&
+ inst->bcwav.loop &&
+ inst->bcwav.loopEnd > inst->bcwav.loopStart &&
+ endSample >= inst->bcwav.loopEnd;
+
+ {
+ N3DSStreamDecodeState nextDecode[2];
+ memcpy(nextDecode, inst->decode, sizeof(nextDecode));
+
+ uint32_t framesToAdvance = (sampleCount + 13u) / 14u;
+ repeat(inst->channelCount, channelIndex) {
+ uint32_t channelSample = startSample;
+ repeat(framesToAdvance, frameIndex) {
+ const uint8_t* frame = inst->streamAdpcm[channelIndex][bufferIndex] + frameIndex * 8u;
+ int16_t scratch[14];
+ nextDecode[channelIndex].predictorScale = frame[0];
+ N3DSAudio_decodeFrame(
+ &inst->bcwav.channels[channelIndex],
+ frame,
+ &nextDecode[channelIndex].hist1,
+ &nextDecode[channelIndex].hist2,
+ scratch
+ );
+ nextDecode[channelIndex].nextFrameIndex++;
+
+ uint32_t remaining = endSample - channelSample;
+ uint32_t frameSamples = remaining > 14u ? 14u : remaining;
+ channelSample += frameSamples;
+ }
+ }
+
+ if (atLoopBoundary) {
+ uint32_t loopFrame = inst->bcwav.loopStart / 14u;
+ repeat(inst->channelCount, channelIndex) {
+ inst->decode[channelIndex].nextFrameIndex = loopFrame;
+ inst->decode[channelIndex].predictorScale = inst->bcwav.channels[channelIndex].loopContext.predictorScale;
+ inst->decode[channelIndex].hist1 = inst->bcwav.channels[channelIndex].loopContext.yn1;
+ inst->decode[channelIndex].hist2 = inst->bcwav.channels[channelIndex].loopContext.yn2;
+ }
+ inst->currentSample = inst->bcwav.loopStart;
+ } else {
+ memcpy(inst->decode, nextDecode, sizeof(nextDecode));
+ inst->currentSample = endSample;
+ }
+ }
+
+ return true;
+}
+
+static bool N3DSAudio_primeStream(N3DSSoundInstance* inst) {
+ inst->streamFinished = false;
+ N3DSAudio_resetInstanceWaveBufState(inst);
+ repeat(N3DS_STREAM_BUFFER_COUNT, i) {
+ bool queued = inst->useNativeAdpcm ? N3DSAudio_fillNativeStreamWaveBuf(inst, i) : N3DSAudio_fillStreamWaveBuf(inst, i);
+ if (!queued) break;
+ ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[i]);
+ if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[i]);
+ }
+ }
+
+ repeat(N3DS_STREAM_BUFFER_COUNT, i) {
+ if (inst->waveBufs[i].status == NDSP_WBUF_QUEUED || inst->waveBufs[i].status == NDSP_WBUF_PLAYING) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static void N3DSAudio_waitForAsyncFillIdleLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst) {
+ if (audio == NULL || inst == NULL || !audio->workerEnabled) return;
+ while (inst->asyncFill.inProgress) {
+ CondVar_Wait(&audio->workerCond, &audio->lock);
+ }
+}
+
+static void N3DSAudio_cancelAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst) {
+ if (audio == NULL || inst == NULL || !inst->isStream) return;
+ N3DSAudio_waitForAsyncFillIdleLocked(audio, inst);
+ inst->streamGeneration += 1u;
+ memset(&inst->asyncFill, 0, sizeof(inst->asyncFill));
+}
+
+static void N3DSAudio_requestAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst, int bufferIndex) {
+ if (audio == NULL || inst == NULL || !audio->workerEnabled || !inst->isStream) return;
+ if (inst->asyncFill.requested || inst->asyncFill.inProgress || inst->asyncFill.ready) return;
+
+ inst->asyncFill.requested = true;
+ inst->asyncFill.inProgress = false;
+ inst->asyncFill.ready = false;
+ inst->asyncFill.success = false;
+ inst->asyncFill.bufferIndex = bufferIndex;
+ inst->asyncFill.generation = inst->streamGeneration;
+ LightEvent_Signal(&audio->workerEvent);
+}
+
+static bool N3DSAudio_applyAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst) {
+ if (audio == NULL || inst == NULL || !inst->asyncFill.ready) return false;
+
+ int bufferIndex = inst->asyncFill.bufferIndex;
+ bool success = inst->asyncFill.success;
+ bool useNativeAdpcm = inst->useNativeAdpcm;
+
+ if (success) {
+ N3DSAudio_applyFillCursor(inst, &inst->asyncFill.nextCursor);
+
+ memset(&inst->waveBufs[bufferIndex], 0, sizeof(inst->waveBufs[bufferIndex]));
+ inst->bufferStartSample[bufferIndex] = inst->asyncFill.bufferStartSample;
+ if (useNativeAdpcm) {
+ inst->adpcmStates[bufferIndex * 2] = inst->asyncFill.adpcmStates[0];
+ inst->waveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[0][bufferIndex];
+ inst->waveBufs[bufferIndex].nsamples = inst->asyncFill.sampleCount;
+ inst->waveBufs[bufferIndex].adpcm_data = &inst->adpcmStates[bufferIndex * 2];
+ inst->waveBufs[bufferIndex].looping = false;
+
+ if (inst->secondaryChannelId >= 0) {
+ memset(&inst->secondaryWaveBufs[bufferIndex], 0, sizeof(inst->secondaryWaveBufs[bufferIndex]));
+ inst->adpcmStates[bufferIndex * 2 + 1] = inst->asyncFill.adpcmStates[1];
+ inst->secondaryWaveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[1][bufferIndex];
+ inst->secondaryWaveBufs[bufferIndex].nsamples = inst->asyncFill.sampleCount;
+ inst->secondaryWaveBufs[bufferIndex].adpcm_data = &inst->adpcmStates[bufferIndex * 2 + 1];
+ inst->secondaryWaveBufs[bufferIndex].looping = false;
+ }
+ } else {
+ inst->waveBufs[bufferIndex].data_pcm16 = inst->streamPcm[bufferIndex];
+ inst->waveBufs[bufferIndex].nsamples = inst->asyncFill.sampleCount;
+ inst->waveBufs[bufferIndex].looping = false;
+ }
+
+ ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[bufferIndex]);
+ if (useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[bufferIndex]);
+ }
+ } else {
+ inst->streamFinished = true;
+ }
+
+ memset(&inst->asyncFill, 0, sizeof(inst->asyncFill));
+ CondVar_Broadcast(&audio->workerCond);
+ return success;
+}
+
+static bool N3DSAudio_hasActiveStreamPlaybackLocked(const N3DSAudioSystem* audio) {
+ if (audio == NULL) return false;
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ const N3DSSoundInstance* inst = &audio->instances[i];
+ if (!inst->active || !inst->isStream) continue;
+ if (inst->streamFinished) continue;
+ return true;
+ }
+ return false;
+}
+
+static bool N3DSAudio_runCachePrewarmJob(N3DSAudioSystem* audio) {
+ if (audio == NULL) return false;
+
+ int32_t soundIndex = -1;
+
+ LightLock_Lock(&audio->lock);
+ if (audio->workerStop) {
+ LightLock_Unlock(&audio->lock);
+ return false;
+ }
+ if (audio->pendingPrewarmSoundCount == 0 || audio->pendingPrewarmByteBudget == 0 || audio->cachedSounds == NULL) {
+ LightLock_Unlock(&audio->lock);
+ return false;
+ }
+ if (N3DSAudio_hasActiveStreamPlaybackLocked(audio)) {
+ LightLock_Unlock(&audio->lock);
+ return false;
+ }
+
+ uint32_t scanned = 0;
+ while (scanned < audio->cachedSoundCount) {
+ uint32_t candidateIndex = audio->cachedSoundPrewarmCursor;
+ audio->cachedSoundPrewarmCursor++;
+ if (audio->cachedSoundPrewarmCursor >= audio->cachedSoundCount) {
+ audio->cachedSoundPrewarmCursor = 0;
+ }
+ scanned++;
+
+ N3DSCachedSound* cachedSound = &audio->cachedSounds[candidateIndex];
+ if (cachedSound->attempted) continue;
+
+ Sound* sound = &audio->base.audioGroups[0]->sond.sounds[candidateIndex];
+ if (!N3DSAudio_shouldPreloadSound(sound)) {
+ cachedSound->attempted = true;
+ continue;
+ }
+
+ cachedSound->attempted = true;
+ soundIndex = (int32_t) candidateIndex;
+ break;
+ }
+
+ if (soundIndex < 0) {
+ audio->pendingPrewarmSoundCount = 0;
+ audio->pendingPrewarmByteBudget = 0;
+ LightLock_Unlock(&audio->lock);
+ return false;
+ }
+ LightLock_Unlock(&audio->lock);
+
+ DataWin* dw = audio->base.audioGroups[0];
+ Sound* sound = &dw->sond.sounds[soundIndex];
+ char* path = NULL;
+ uint8_t* blob = NULL;
+ uint32_t blobSize = 0;
+ N3DSBcwav bcwav;
+ bool loaded = false;
+ bool blobOwned = false;
+ memset(&bcwav, 0, sizeof(bcwav));
+
+ const uint8_t* packedBlob = NULL;
+ uint32_t packedBlobSize = 0;
+ const N3DSPackedSoundBankEntry* packedEntry = N3DSAudio_getPackedSoundEntry(audio, soundIndex);
+ if (!N3DSAudio_soundLooksLikeMusic(sound) &&
+ packedEntry != NULL &&
+ !N3DSAudio_packedSoundEntryIsPcm16(packedEntry) &&
+ N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize) &&
+ N3DSAudio_parseBcwavBlob(packedBlob, packedBlobSize, &bcwav) &&
+ N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) {
+ blob = (uint8_t*) packedBlob;
+ blobSize = packedBlobSize;
+ loaded = true;
+ } else {
+ path = N3DSAudio_resolveSoundPath(audio, sound, soundIndex);
+ if (path == NULL) {
+ path = N3DSAudio_tryResolveGeneratedSoundPath(audio, sound, soundIndex);
+ }
+
+ if (path != NULL &&
+ N3DSAudio_readFileFully(path, &blob, &blobSize) &&
+ N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav) &&
+ N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) {
+ loaded = true;
+ blobOwned = true;
+ }
+ }
+
+ LightLock_Lock(&audio->lock);
+ N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex];
+ if (loaded && (!blobOwned || audio->cachedSoundBytes + blobSize <= audio->maxCachedSoundBytes)) {
+ cachedSound->available = true;
+ cachedSound->path = path;
+ cachedSound->blob = blob;
+ cachedSound->blobOwned = blobOwned;
+ cachedSound->blobSize = blobSize;
+ cachedSound->bcwav = bcwav;
+ if (blobOwned) audio->cachedSoundBytes += blobSize;
+ if (audio->pendingPrewarmSoundCount > 0) audio->pendingPrewarmSoundCount--;
+ if (audio->pendingPrewarmByteBudget > blobSize) audio->pendingPrewarmByteBudget -= blobSize;
+ else audio->pendingPrewarmByteBudget = 0;
+ path = NULL;
+ blob = NULL;
+ }
+ LightLock_Unlock(&audio->lock);
+
+ free(path);
+ if (blobOwned) free(blob);
+ return true;
+}
+
+static bool N3DSAudio_hasPendingAsyncFillLocked(const N3DSAudioSystem* audio) {
+ if (audio == NULL || !audio->workerEnabled) return false;
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ const N3DSSoundInstance* inst = &audio->instances[i];
+ if (!inst->active || !inst->isStream) continue;
+ if (inst->asyncFill.requested || inst->asyncFill.inProgress) return true;
+ }
+ return false;
+}
+
+static void N3DSAudio_streamWorkerMain(void* arg) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) arg;
+ if (audio == NULL) threadExit(0);
+
+ while (true) {
+ LightEvent_Wait(&audio->workerEvent);
+
+ while (true) {
+ int32_t jobSlot = -1;
+ int bufferIndex = -1;
+ uint32_t generation = 0;
+ bool useNativeAdpcm = false;
+ N3DSStreamFillCursor cursor;
+ memset(&cursor, 0, sizeof(cursor));
+
+ LightLock_Lock(&audio->lock);
+ if (audio->workerStop) {
+ LightLock_Unlock(&audio->lock);
+ threadExit(0);
+ }
+
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSSoundInstance* inst = &audio->instances[i];
+ if (!inst->active || !inst->isStream) continue;
+ if (!inst->asyncFill.requested || inst->asyncFill.inProgress || inst->asyncFill.ready) continue;
+ inst->asyncFill.inProgress = true;
+ jobSlot = (int32_t) i;
+ bufferIndex = inst->asyncFill.bufferIndex;
+ generation = inst->asyncFill.generation;
+ useNativeAdpcm = inst->useNativeAdpcm;
+ N3DSAudio_captureFillCursor(inst, &cursor);
+ break;
+ }
+ LightLock_Unlock(&audio->lock);
+
+ if (jobSlot < 0) {
+ if (!N3DSAudio_runCachePrewarmJob(audio)) break;
+ continue;
+ }
+
+ N3DSSoundInstance* inst = &audio->instances[jobSlot];
+ uint32_t startSample = 0;
+ uint32_t producedSamples = 0;
+ ndspAdpcmData adpcmStates[2];
+ memset(adpcmStates, 0, sizeof(adpcmStates));
+ bool success = useNativeAdpcm
+ ? N3DSAudio_fillNativeStreamWaveBufAsync(inst, bufferIndex, &cursor, &startSample, &producedSamples, adpcmStates)
+ : N3DSAudio_fillStreamWaveBufAsync(inst, bufferIndex, &cursor, &startSample, &producedSamples);
+
+ LightLock_Lock(&audio->lock);
+ inst = &audio->instances[jobSlot];
+ if (inst->active &&
+ inst->isStream &&
+ inst->asyncFill.inProgress &&
+ inst->asyncFill.requested &&
+ inst->asyncFill.generation == generation &&
+ inst->asyncFill.bufferIndex == bufferIndex) {
+ inst->asyncFill.inProgress = false;
+ inst->asyncFill.ready = true;
+ inst->asyncFill.success = success;
+ inst->asyncFill.bufferStartSample = startSample;
+ inst->asyncFill.sampleCount = producedSamples;
+ inst->asyncFill.nextCursor = cursor;
+ inst->asyncFill.adpcmStates[0] = adpcmStates[0];
+ inst->asyncFill.adpcmStates[1] = adpcmStates[1];
+ }
+ CondVar_Broadcast(&audio->workerCond);
+ bool morePending = N3DSAudio_hasPendingAsyncFillLocked(audio);
+ bool shouldStop = audio->workerStop;
+ LightLock_Unlock(&audio->lock);
+
+ if (shouldStop) threadExit(0);
+ if (!morePending) break;
+ }
+ }
+}
+
+static bool N3DSAudio_startFileNativeAdpcmStreamPlayback(
+ N3DSAudioSystem* audio,
+ N3DSSoundInstance* inst,
+ const char* path,
+ const N3DSBcwav* bcwav
+) {
+ if (audio == NULL || inst == NULL || path == NULL || bcwav == NULL) return false;
+ if (!N3DSAudio_canUseNativeAdpcmPlayback(bcwav, inst->loop)) return false;
+
+ FILE* file = fopen(path, "rb");
+ if (file == NULL) return false;
+ N3DSAudio_configureFileBuffer(file);
+
+ inst->streamFile = file;
+ inst->channelId = N3DSAudio_acquireChannel(audio);
+ if (inst->channelId < 0) {
+ N3DSAudio_cleanupPartialStreamResources(audio, inst);
+ return false;
+ }
+ if (bcwav->channelCount == 2) {
+ inst->secondaryChannelId = N3DSAudio_acquireChannel(audio);
+ if (inst->secondaryChannelId < 0) {
+ N3DSAudio_cleanupPartialStreamResources(audio, inst);
+ return false;
+ }
+ }
+
+ inst->isStream = true;
+ inst->useNativeAdpcm = true;
+ inst->sampleRate = bcwav->sampleRate;
+ inst->sampleCount = bcwav->sampleCount;
+ inst->channelCount = bcwav->channelCount;
+ inst->baseRate = (float) bcwav->sampleRate;
+ inst->bcwav = *bcwav;
+
+ uint32_t maxFrames = (N3DS_STREAM_CHUNK_SAMPLES + 13u) / 14u;
+ uint32_t bytesPerBuffer = maxFrames * 8u;
+ repeat(inst->channelCount, channelIndex) {
+ repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) {
+ inst->streamAdpcm[channelIndex][bufferIndex] = linearAlloc(bytesPerBuffer);
+ if (inst->streamAdpcm[channelIndex][bufferIndex] == NULL) {
+ N3DSAudio_cleanupPartialStreamResources(audio, inst);
+ return false;
+ }
+ }
+ }
+
+ N3DSAudio_streamResetDecoder(inst);
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ if (!N3DSAudio_primeStream(inst)) {
+ N3DSAudio_cleanupPartialStreamResources(audio, inst);
+ return false;
+ }
+ return true;
+}
+
+static bool N3DSAudio_startFileStreamPlayback(
+ N3DSAudioSystem* audio,
+ N3DSSoundInstance* inst,
+ const char* path,
+ const N3DSBcwav* bcwav
+) {
+ if (audio == NULL || inst == NULL || path == NULL || bcwav == NULL) return false;
+ if (bcwav->sampleRate == 0 || bcwav->channelCount == 0 || bcwav->channelCount > 2) return false;
+
+ FILE* file = fopen(path, "rb");
+ if (file == NULL) return false;
+ N3DSAudio_configureFileBuffer(file);
+
+ inst->channelId = N3DSAudio_acquireChannel(audio);
+ if (inst->channelId < 0) {
+ fclose(file);
+ return false;
+ }
+
+ inst->streamFile = file;
+ inst->isStream = true;
+ inst->sampleRate = bcwav->sampleRate;
+ inst->sampleCount = bcwav->sampleCount;
+ inst->channelCount = bcwav->channelCount;
+ inst->baseRate = (float) bcwav->sampleRate;
+ inst->bcwav = *bcwav;
+
+ size_t stride = bcwav->channelCount;
+ size_t samplesPerBuffer = (size_t) N3DS_STREAM_CHUNK_SAMPLES * stride;
+ repeat(N3DS_STREAM_BUFFER_COUNT, i) {
+ inst->streamPcm[i] = linearAlloc(samplesPerBuffer * sizeof(int16_t));
+ if (inst->streamPcm[i] == NULL) {
+ return false;
+ }
+ }
+
+ N3DSAudio_streamResetDecoder(inst);
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ return N3DSAudio_primeStream(inst);
+}
+
+static void N3DSAudio_init(AudioSystem* base, DataWin* dataWin, FileSystem* fileSystem) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ if (audio == NULL || audio->initialized) return;
+
+ bool isNew3DS = false;
+ LightLock_Init(&audio->lock);
+ LightLock_Init(&audio->missingPathLock);
+ CondVar_Init(&audio->workerCond);
+ LightEvent_Init(&audio->workerEvent, RESET_STICKY);
+ audio->fileSystem = fileSystem;
+ audio->masterGain = 1.0f;
+ audio->base.audioGroups = safeCalloc(1, sizeof(DataWin*));
+ audio->base.audioGroups[0] = dataWin;
+ if (R_SUCCEEDED(APT_CheckNew3DS(&isNew3DS))) {
+ audio->isNew3DS = isNew3DS;
+ }
+#if N3DS_FORCE_OLD3DS_MODE
+ audio->isNew3DS = false;
+#endif
+ audio->maxCachedSoundBytes = audio->isNew3DS ? N3DS_MAX_PRELOADED_SFX_BYTES_NEW3DS : N3DS_MAX_PRELOADED_SFX_BYTES_OLD3DS;
+ audio->roomPrewarmSoundCount = audio->isNew3DS ? N3DS_ROOM_PREWARM_SOUND_COUNT_NEW3DS : N3DS_ROOM_PREWARM_SOUND_COUNT_OLD3DS;
+ audio->roomPrewarmByteBudget = audio->isNew3DS ? N3DS_ROOM_PREWARM_BYTE_BUDGET_NEW3DS : N3DS_ROOM_PREWARM_BYTE_BUDGET_OLD3DS;
+ audio->backgroundPrewarmSoundCount = audio->isNew3DS ? N3DS_BACKGROUND_PREWARM_SOUND_COUNT_NEW3DS : N3DS_BACKGROUND_PREWARM_SOUND_COUNT_OLD3DS;
+ audio->backgroundPrewarmByteBudget = audio->isNew3DS ? N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_NEW3DS : N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_OLD3DS;
+ N3DSAudio_loadPackedSoundBank(audio);
+
+ ndspInit();
+ ndspSetOutputMode(NDSP_OUTPUT_STEREO);
+ N3DSDebugLog_event(
+ "audio",
+ "init model=%s cacheCapKB=%lu packedBankKB=%lu",
+ audio->isNew3DS ? "new3ds" : "old3ds",
+ (unsigned long) (audio->maxCachedSoundBytes / 1024u),
+ (unsigned long) (audio->packedSoundBankSize / 1024u)
+ );
+ fprintf(
+ stderr,
+ "N3DSAudio: init complete, sounds=%lu, model=%s, cacheCap=%luKB, packedBank=%luKB, roomPrewarm=%lu/%luKB, bgPrewarm=%lu/%luKB\n",
+ (unsigned long) dataWin->sond.count,
+ audio->isNew3DS ? "new3ds" : "old3ds",
+ (unsigned long) (audio->maxCachedSoundBytes / 1024u),
+ (unsigned long) (audio->packedSoundBankSize / 1024u),
+ (unsigned long) audio->roomPrewarmSoundCount,
+ (unsigned long) (audio->roomPrewarmByteBudget / 1024u),
+ (unsigned long) audio->backgroundPrewarmSoundCount,
+ (unsigned long) (audio->backgroundPrewarmByteBudget / 1024u)
+ );
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ ndspChnReset((int) i);
+ ndspChnSetInterp((int) i, NDSP_INTERP_LINEAR);
+ }
+#if N3DS_EAGER_SFX_PRELOAD
+ N3DSAudio_preloadSounds(audio, dataWin);
+#else
+ audio->cachedSounds = safeCalloc(dataWin->sond.count, sizeof(N3DSCachedSound));
+ audio->cachedSoundCount = dataWin->sond.count;
+ fprintf(stderr, "N3DSAudio: eager preload disabled; sounds will cache on first use or the worker thread\n");
+#endif
+
+ audio->workerEnabled = false;
+#if N3DS_ENABLE_STREAM_WORKER
+ if (R_SUCCEEDED(APT_SetAppCpuTimeLimit(N3DS_STREAM_WORKER_CPU_LIMIT))) {
+ audio->workerThread = threadCreate(
+ N3DSAudio_streamWorkerMain,
+ audio,
+ N3DS_STREAM_WORKER_STACK_SIZE,
+ N3DS_STREAM_WORKER_PRIORITY,
+ N3DS_STREAM_WORKER_CORE_ID,
+ false
+ );
+ audio->workerEnabled = audio->workerThread != NULL;
+ }
+#endif
+ N3DSDebugLog_event(
+ "audio",
+ "worker enabled=%d cpuLimit=%lu nativeAdpcm=%d",
+ audio->workerEnabled ? 1 : 0,
+ (unsigned long) N3DS_STREAM_WORKER_CPU_LIMIT,
+ N3DS_FORCE_PCM_BCWAV_PLAYBACK ? 0 : 1
+ );
+ audio->initialized = true;
+}
+
+static void N3DSAudio_destroy(AudioSystem* base) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ if (audio->workerEnabled) {
+ LightLock_Lock(&audio->lock);
+ audio->workerStop = true;
+ LightLock_Unlock(&audio->lock);
+ LightEvent_Signal(&audio->workerEvent);
+ threadJoin(audio->workerThread, U64_MAX);
+ threadFree(audio->workerThread);
+ audio->workerThread = NULL;
+ audio->workerEnabled = false;
+ }
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSAudio_releaseInstance(audio, &audio->instances[i]);
+ }
+ repeat(N3DS_MAX_STREAMS, i) {
+ free(audio->streams[i].path);
+ }
+ repeat(audio->cachedSoundCount, i) {
+ N3DSAudio_releaseCachedSound(&audio->cachedSounds[i]);
+ }
+ free(audio->cachedSounds);
+ free(audio->packedSoundBankEntries);
+ free(audio->packedSoundBankData);
+ ndspExit();
+ free(audio->base.audioGroups);
+ free(audio);
+
+ repeat(gN3DSMissingAudioPathCount, i) {
+ free(gN3DSMissingAudioPaths[i]);
+ gN3DSMissingAudioPaths[i] = NULL;
+ }
+ gN3DSMissingAudioPathCount = 0;
+}
+
+static void N3DSAudio_update(AudioSystem* base, MAYBE_UNUSED float deltaTime) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ u64 nowTick = svcGetSystemTick();
+ LightLock_Lock(&audio->lock);
+#if !N3DS_EAGER_SFX_PRELOAD
+ N3DSAudio_requestCachePrewarmLocked(audio, audio->backgroundPrewarmSoundCount, audio->backgroundPrewarmByteBudget);
+#endif
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSSoundInstance* inst = &audio->instances[i];
+ if (!inst->active) continue;
+
+ if (!inst->isStream) {
+ bool finished = N3DSAudio_nonStreamPlaybackFinished(inst, nowTick);
+ if (finished && !inst->loop) {
+ N3DSAudio_releaseInstance(audio, inst);
+ }
+ continue;
+ }
+
+ bool anyQueued = false;
+ repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) {
+ ndspWaveBuf* waveBuf = &inst->waveBufs[bufferIndex];
+ bool secondaryReadyToReuse = true;
+ if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ secondaryReadyToReuse = inst->secondaryWaveBufs[bufferIndex].status == NDSP_WBUF_DONE;
+ }
+ if (waveBuf->status == NDSP_WBUF_DONE && secondaryReadyToReuse) {
+ waveBuf->status = NDSP_WBUF_FREE;
+ if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ inst->secondaryWaveBufs[bufferIndex].status = NDSP_WBUF_FREE;
+ }
+ }
+ if (waveBuf->status == NDSP_WBUF_FREE &&
+ inst->asyncFill.ready &&
+ inst->asyncFill.bufferIndex == (int) bufferIndex) {
+ N3DSAudio_applyAsyncFillLocked(audio, inst);
+ }
+ if (waveBuf->status == NDSP_WBUF_FREE &&
+ !inst->streamFinished &&
+ !inst->asyncFill.requested &&
+ !inst->asyncFill.inProgress &&
+ !inst->asyncFill.ready) {
+ if (audio->workerEnabled) N3DSAudio_requestAsyncFillLocked(audio, inst, (int) bufferIndex);
+ else {
+ bool refilled = inst->useNativeAdpcm ?
+ N3DSAudio_fillNativeStreamWaveBuf(inst, (int) bufferIndex) :
+ N3DSAudio_fillStreamWaveBuf(inst, (int) bufferIndex);
+ if (!inst->streamFinished && refilled) {
+ ndspChnWaveBufAdd(inst->channelId, waveBuf);
+ if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) {
+ ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[bufferIndex]);
+ }
+ }
+ }
+ }
+ bool primaryBusy = waveBuf->status != NDSP_WBUF_FREE;
+ bool secondaryBusy = inst->useNativeAdpcm &&
+ inst->secondaryChannelId >= 0 &&
+ inst->secondaryWaveBufs[bufferIndex].status != NDSP_WBUF_FREE;
+ if (primaryBusy || secondaryBusy) {
+ anyQueued = true;
+ }
+ }
+
+ if (!anyQueued &&
+ !inst->asyncFill.requested &&
+ !inst->asyncFill.inProgress &&
+ !inst->asyncFill.ready) {
+ inst->streamFinished = true;
+ N3DSAudio_releaseInstance(audio, inst);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static int32_t N3DSAudio_playSound(AudioSystem* base, int32_t soundIndex, MAYBE_UNUSED int32_t priority, bool loop) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ char* path = NULL;
+ bool useStreamingPath = soundIndex >= N3DS_AUDIO_STREAM_INDEX_BASE;
+ bool useMusicStreamPath = false;
+ N3DSBcwav streamBcwav;
+ memset(&streamBcwav, 0, sizeof(streamBcwav));
+ Sound* sound = NULL;
+ N3DSCachedSound* cachedSound = NULL;
+ const uint8_t* packedBlob = NULL;
+ uint32_t packedBlobSize = 0;
+ bool hasPackedBlob = false;
+
+ if (useStreamingPath) {
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundIndex);
+ if (stream == NULL) {
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ path = safeStrdup(stream->path);
+ streamBcwav = stream->bcwav;
+ } else {
+ DataWin* dw = audio->base.audioGroups[0];
+ if (soundIndex < 0 || (uint32_t) soundIndex >= dw->sond.count) {
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ sound = &dw->sond.sounds[soundIndex];
+ cachedSound = N3DSAudio_getCachedSoundIfAvailable(audio, soundIndex);
+ if (!N3DSAudio_soundLooksLikeMusic(sound)) {
+ hasPackedBlob = N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize);
+ }
+ useMusicStreamPath = sound != NULL && loop && N3DSAudio_soundLooksLikeMusic(sound);
+ if (useMusicStreamPath || (!hasPackedBlob && cachedSound == NULL)) {
+ const char* resolvedPath = N3DSAudio_getResolvedCachedSoundPath(audio, soundIndex, sound);
+ if (resolvedPath != NULL) path = safeStrdup(resolvedPath);
+ }
+ }
+
+ if (!useStreamingPath && !useMusicStreamPath && loop && N3DSAudio_pathLooksLikeBundledMusic(path)) {
+ useMusicStreamPath = true;
+ }
+
+ if (path == NULL && !hasPackedBlob && cachedSound == NULL) {
+ if (audio->lastResolveFailureSound != soundIndex) {
+ audio->lastResolveFailureSound = soundIndex;
+ N3DSDebugLog_event("audio_fail", "resolve failed sound=%ld", (long) soundIndex);
+ fprintf(
+ stderr,
+ "N3DSAudio: could not resolve sound %ld name=%s file=%s audioFile=%ld group=%ld\n",
+ (long) soundIndex,
+ sound != NULL && sound->name != NULL ? sound->name : "",
+ sound != NULL && sound->file != NULL ? sound->file : "",
+ sound != NULL ? (long) sound->audioFile : -1L,
+ sound != NULL ? (long) sound->audioGroup : -1L
+ );
+ }
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ bool shouldReplaceExistingInstance = useStreamingPath || (useMusicStreamPath && loop);
+ if (shouldReplaceExistingInstance) {
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundIndex) {
+ N3DSAudio_releaseInstance(audio, &audio->instances[i]);
+ }
+ }
+ }
+
+ N3DSSoundInstance* inst = N3DSAudio_findFreeInstance(audio, useStreamingPath || useMusicStreamPath);
+ if (inst == NULL) {
+ free(path);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ int32_t slot = (int32_t) (inst - audio->instances);
+ memset(inst, 0, sizeof(*inst));
+ inst->active = true;
+ inst->loop = loop;
+ inst->soundIndex = soundIndex;
+ inst->instanceId = N3DS_SOUND_INSTANCE_ID_BASE + slot;
+ inst->channelId = -1;
+ inst->secondaryChannelId = -1;
+ inst->gain = N3DSAudio_sanitizeGain(sound != NULL ? sound->volume : 1.0f);
+ inst->pitch = N3DSAudio_sanitizePitch((sound != NULL && sound->pitch > 0.0f) ? sound->pitch : 1.0f);
+ inst->streamGeneration = 1u;
+
+ if (useStreamingPath || useMusicStreamPath) {
+ N3DSBcwav musicBcwav;
+ memset(&musicBcwav, 0, sizeof(musicBcwav));
+
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundIndex);
+ if (useStreamingPath) {
+ if (stream == NULL) {
+ free(path);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ inst->gain = stream->gain;
+ inst->pitch = stream->pitch;
+ musicBcwav = stream->bcwav;
+ } else {
+ if (!N3DSAudio_parseBcwavFile(path, &musicBcwav)) {
+ fprintf(stderr, "N3DSAudio: failed to parse music header %s\n", path);
+ free(path);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ }
+
+ bool started = N3DSAudio_startFileNativeAdpcmStreamPlayback(audio, inst, path, &musicBcwav);
+ if (!started) {
+ N3DSDebugLog_event("audio_fail", "stream start failed sound=%ld path=%s", (long) soundIndex, path);
+ free(path);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ N3DSDebugLog_event(
+ "audio_start",
+ "stream sound=%ld instance=%ld native=%d loop=%d rate=%lu ch=%lu path=%s",
+ (long) soundIndex,
+ (long) inst->instanceId,
+ inst->useNativeAdpcm ? 1 : 0,
+ loop ? 1 : 0,
+ (unsigned long) inst->sampleRate,
+ (unsigned long) inst->channelCount,
+ path
+ );
+ free(path);
+ int32_t instanceId = inst->instanceId;
+ LightLock_Unlock(&audio->lock);
+ return instanceId;
+ }
+
+ const uint8_t* blob = NULL;
+ uint32_t blobSize = 0;
+ N3DSBcwav bcwav;
+ const N3DSPackedSoundBankEntry* packedEntry = hasPackedBlob ? N3DSAudio_getPackedSoundEntry(audio, soundIndex) : NULL;
+ bool uncachedBlobLinear = false;
+ memset(&bcwav, 0, sizeof(bcwav));
+
+ if (cachedSound != NULL) {
+ blob = cachedSound->blob;
+ blobSize = cachedSound->blobSize;
+ bcwav = cachedSound->bcwav;
+ } else if (hasPackedBlob && packedEntry != NULL && N3DSAudio_packedSoundEntryIsPcm16(packedEntry)) {
+ uint8_t channelCount = N3DSAudio_packedSoundEntryChannelCount(packedEntry);
+ size_t expectedBytes = (size_t) packedEntry->sampleCount * channelCount * sizeof(int16_t);
+ if (packedEntry->sampleRate == 0 ||
+ packedEntry->sampleCount == 0 ||
+ channelCount == 0 ||
+ channelCount > 2 ||
+ expectedBytes != packedBlobSize) {
+ N3DSDebugLog_event("audio_fail", "bank pcm metadata invalid sound=%ld", (long) soundIndex);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ int16_t* pcm = (int16_t*) N3DSAudio_cloneBlobToLinear(packedBlob, packedBlobSize);
+ if (pcm == NULL || !N3DSAudio_startOwnedPcmPlayback(audio, inst, pcm, packedEntry->sampleRate, packedEntry->sampleCount, channelCount, loop)) {
+ if (pcm != NULL) linearFree(pcm);
+ N3DSDebugLog_event("audio_fail", "bank pcm start failed sound=%ld", (long) soundIndex);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ int32_t instanceId = inst->instanceId;
+ LightLock_Unlock(&audio->lock);
+ return instanceId;
+ } else if (hasPackedBlob) {
+ blob = packedBlob;
+ blobSize = packedBlobSize;
+ if (!N3DSAudio_getCachedSoundHeader(audio, soundIndex, sound, &bcwav) &&
+ !N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav)) {
+ N3DSDebugLog_event("audio_fail", "bank parse failed sound=%ld", (long) soundIndex);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ } else {
+ uint8_t* uncachedBlob = NULL;
+ if (!N3DSAudio_readFileFullyLinear(path, &uncachedBlob, &blobSize)) {
+ N3DSDebugLog_event("audio_fail", "read failed path=%s sound=%ld", path, (long) soundIndex);
+ fprintf(stderr, "N3DSAudio: failed to read %s\n", path);
+ free(path);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ blob = uncachedBlob;
+ uncachedBlobLinear = true;
+ if (!N3DSAudio_getCachedSoundHeader(audio, soundIndex, sound, &bcwav) &&
+ !N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav)) {
+ N3DSDebugLog_event("audio_fail", "blob parse failed sound=%ld path=%s", (long) soundIndex, path);
+ fprintf(stderr, "N3DSAudio: failed to parse in-memory BCWAV\n");
+ linearFree((void*) blob);
+ free(path);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ }
+ free(path);
+ if (cachedSound == NULL && soundIndex >= 0 && (uint32_t) soundIndex < audio->cachedSoundCount) {
+ N3DSCachedSound* headerCache = &audio->cachedSounds[soundIndex];
+ headerCache->headerAttempted = true;
+ headerCache->headerAvailable = true;
+ headerCache->bcwav = bcwav;
+ }
+
+ if (N3DSAudio_canUseNativeAdpcmPlayback(&bcwav, loop)) {
+ bool started = false;
+ if (cachedSound != NULL) {
+ started = N3DSAudio_startNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav);
+ } else if (hasPackedBlob) {
+ started = N3DSAudio_startNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav);
+ } else {
+ started = uncachedBlobLinear
+ ? N3DSAudio_startOwnedLinearNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav)
+ : N3DSAudio_startNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav);
+ }
+ if (!started) {
+ N3DSDebugLog_event("audio_fail", "native start failed sound=%ld", (long) soundIndex);
+ if (cachedSound == NULL && !hasPackedBlob) {
+ if (uncachedBlobLinear) linearFree((void*) blob);
+ else free((void*) blob);
+ }
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ int32_t instanceId = inst->instanceId;
+ LightLock_Unlock(&audio->lock);
+ return instanceId;
+ }
+
+ int16_t* pcm = N3DSAudio_decodeBcwavToPcm(blob, &bcwav);
+ if (cachedSound == NULL && !hasPackedBlob) {
+ if (uncachedBlobLinear) linearFree((void*) blob);
+ else free((void*) blob);
+ }
+ if (pcm == NULL) {
+ N3DSDebugLog_event("audio_fail", "pcm decode failed sound=%ld", (long) soundIndex);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+ if (!N3DSAudio_startOwnedPcmPlayback(audio, inst, pcm, bcwav.sampleRate, bcwav.sampleCount, bcwav.channelCount, loop)) {
+ linearFree(pcm);
+ N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ int32_t instanceId = inst->instanceId;
+ LightLock_Unlock(&audio->lock);
+ return instanceId;
+}
+
+static void N3DSAudio_prewarmRoom(AudioSystem* base, MAYBE_UNUSED Runner* runner) {
+#if !N3DS_EAGER_SFX_PRELOAD
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ N3DSAudio_requestCachePrewarmLocked(audio, audio->roomPrewarmSoundCount, audio->roomPrewarmByteBudget);
+ LightLock_Unlock(&audio->lock);
+#else
+ (void) base;
+ (void) runner;
+#endif
+}
+
+static void N3DSAudio_stopSound(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ if (inst != NULL) N3DSAudio_releaseInstance(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) {
+ N3DSAudio_releaseInstance(audio, &audio->instances[i]);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static void N3DSAudio_stopAll(AudioSystem* base) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSAudio_releaseInstance(audio, &audio->instances[i]);
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static bool N3DSAudio_isPlaying(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ bool isPlaying = inst != NULL && ndspChnIsPlaying(inst->channelId) && !inst->paused;
+ LightLock_Unlock(&audio->lock);
+ return isPlaying;
+ }
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) {
+ bool isPlaying = ndspChnIsPlaying(audio->instances[i].channelId) && !audio->instances[i].paused;
+ LightLock_Unlock(&audio->lock);
+ return isPlaying;
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+ return false;
+}
+
+static void N3DSAudio_pauseSound(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ if (inst != NULL) {
+ inst->paused = true;
+ ndspChnSetPaused(inst->channelId, true);
+ if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true);
+ }
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSSoundInstance* inst = &audio->instances[i];
+ if (inst->active && inst->soundIndex == soundOrInstance) {
+ inst->paused = true;
+ ndspChnSetPaused(inst->channelId, true);
+ if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static void N3DSAudio_resumeSound(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ if (inst != NULL) {
+ inst->paused = false;
+ ndspChnSetPaused(inst->channelId, false);
+ if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, false);
+ }
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSSoundInstance* inst = &audio->instances[i];
+ if (inst->active && inst->soundIndex == soundOrInstance) {
+ inst->paused = false;
+ ndspChnSetPaused(inst->channelId, false);
+ if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, false);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static void N3DSAudio_pauseAll(AudioSystem* base) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active) {
+ audio->instances[i].paused = true;
+ ndspChnSetPaused(audio->instances[i].channelId, true);
+ if (audio->instances[i].secondaryChannelId >= 0) ndspChnSetPaused(audio->instances[i].secondaryChannelId, true);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static void N3DSAudio_resumeAll(AudioSystem* base) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active) {
+ audio->instances[i].paused = false;
+ ndspChnSetPaused(audio->instances[i].channelId, false);
+ if (audio->instances[i].secondaryChannelId >= 0) ndspChnSetPaused(audio->instances[i].secondaryChannelId, false);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static void N3DSAudio_setSoundGain(AudioSystem* base, int32_t soundOrInstance, float gain, MAYBE_UNUSED uint32_t timeMs) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ gain = N3DSAudio_sanitizeGain(gain);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ if (inst == NULL) {
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ inst->gain = gain;
+ if (inst->soundIndex >= N3DS_AUDIO_STREAM_INDEX_BASE) {
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, inst->soundIndex);
+ if (stream != NULL) stream->gain = gain;
+ }
+ N3DSAudio_applyMix(audio, inst);
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance);
+ if (stream != NULL) stream->gain = gain;
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSSoundInstance* inst = &audio->instances[i];
+ if (inst->active && inst->soundIndex == soundOrInstance) {
+ inst->gain = gain;
+ N3DSAudio_applyMix(audio, inst);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static float N3DSAudio_getSoundGain(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ float gain = inst != NULL ? inst->gain : 0.0f;
+ LightLock_Unlock(&audio->lock);
+ return gain;
+ }
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) {
+ float gain = audio->instances[i].gain;
+ LightLock_Unlock(&audio->lock);
+ return gain;
+ }
+ }
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance);
+ if (stream != NULL) {
+ float gain = stream->gain;
+ LightLock_Unlock(&audio->lock);
+ return gain;
+ }
+ LightLock_Unlock(&audio->lock);
+ return 0.0f;
+}
+
+static void N3DSAudio_setSoundPitch(AudioSystem* base, int32_t soundOrInstance, float pitch) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ pitch = N3DSAudio_sanitizePitch(pitch);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ if (inst == NULL) {
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ inst->pitch = pitch;
+ if (inst->soundIndex >= N3DS_AUDIO_STREAM_INDEX_BASE) {
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, inst->soundIndex);
+ if (stream != NULL) stream->pitch = pitch;
+ }
+ N3DSAudio_applyMix(audio, inst);
+ N3DSAudio_refreshNonStreamReleaseTick(inst);
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance);
+ if (stream != NULL) stream->pitch = pitch;
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ N3DSSoundInstance* inst = &audio->instances[i];
+ if (inst->active && inst->soundIndex == soundOrInstance) {
+ inst->pitch = pitch;
+ N3DSAudio_applyMix(audio, inst);
+ N3DSAudio_refreshNonStreamReleaseTick(inst);
+ }
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static float N3DSAudio_getSoundPitch(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ float pitch = inst != NULL ? inst->pitch : 1.0f;
+ LightLock_Unlock(&audio->lock);
+ return pitch;
+ }
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) {
+ float pitch = audio->instances[i].pitch;
+ LightLock_Unlock(&audio->lock);
+ return pitch;
+ }
+ }
+ N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance);
+ if (stream != NULL) {
+ float pitch = stream->pitch;
+ LightLock_Unlock(&audio->lock);
+ return pitch;
+ }
+ LightLock_Unlock(&audio->lock);
+ return 1.0f;
+}
+
+static float N3DSAudio_getTrackPosition(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ N3DSSoundInstance* inst = NULL;
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ } else {
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) {
+ inst = &audio->instances[i];
+ break;
+ }
+ }
+ }
+ if (inst == NULL || inst->sampleRate == 0) {
+ LightLock_Unlock(&audio->lock);
+ return 0.0f;
+ }
+
+ if (inst->useNativeAdpcm) {
+ uint16_t currentSeq = ndspChnGetWaveBufSeq(inst->channelId);
+ repeat(N3DS_STREAM_BUFFER_COUNT, i) {
+ if (inst->waveBufs[i].sequence_id == currentSeq) {
+ uint32_t samplePos = ndspChnGetSamplePos(inst->channelId);
+ float position = (float) (inst->bufferStartSample[i] + samplePos) / (float) inst->sampleRate;
+ LightLock_Unlock(&audio->lock);
+ return position;
+ }
+ }
+ float position = (float) ndspChnGetSamplePos(inst->channelId) / (float) inst->sampleRate;
+ LightLock_Unlock(&audio->lock);
+ return position;
+ }
+
+ if (!inst->isStream) {
+ float position = (float) ndspChnGetSamplePos(inst->channelId) / (float) inst->sampleRate;
+ LightLock_Unlock(&audio->lock);
+ return position;
+ }
+
+ uint16_t currentSeq = ndspChnGetWaveBufSeq(inst->channelId);
+ repeat(N3DS_STREAM_BUFFER_COUNT, i) {
+ if (inst->waveBufs[i].sequence_id == currentSeq) {
+ uint32_t samplePos = ndspChnGetSamplePos(inst->channelId);
+ float position = (float) (inst->bufferStartSample[i] + samplePos) / (float) inst->sampleRate;
+ LightLock_Unlock(&audio->lock);
+ return position;
+ }
+ }
+
+ float position = (float) inst->currentSample / (float) inst->sampleRate;
+ LightLock_Unlock(&audio->lock);
+ return position;
+}
+
+static void N3DSAudio_setTrackPosition(AudioSystem* base, int32_t soundOrInstance, float positionSeconds) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ N3DSSoundInstance* inst = NULL;
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ } else {
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) {
+ inst = &audio->instances[i];
+ break;
+ }
+ }
+ }
+ if (inst == NULL || inst->sampleRate == 0) {
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+
+ uint32_t targetSample = (uint32_t) (positionSeconds * (float) inst->sampleRate);
+ if (inst->useNativeAdpcm && !inst->isStream) {
+ if (targetSample != 0) {
+ N3DSDebugLog_event("audio_seek", "reject native instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample);
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ N3DSDebugLog_event("audio_seek", "native instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample);
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ if (!N3DSAudio_primeNativeAdpcm(inst)) {
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ if (inst->paused) {
+ ndspChnSetPaused(inst->channelId, true);
+ if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true);
+ }
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+
+ if (inst->useNativeAdpcm && inst->isStream) {
+ N3DSAudio_cancelAsyncFillLocked(audio, inst);
+ if (!N3DSAudio_isFrameAlignedSample(targetSample)) {
+ targetSample = (targetSample / 14u) * 14u;
+ }
+ N3DSDebugLog_event("audio_seek", "native-stream instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample);
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ if (!N3DSAudio_streamSeekSamples(inst, targetSample)) {
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ if (!N3DSAudio_primeStream(inst)) {
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ if (inst->paused) {
+ ndspChnSetPaused(inst->channelId, true);
+ if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true);
+ }
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+
+ if (!inst->isStream) {
+ N3DSDebugLog_event("audio_seek", "pcm instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample);
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ N3DSAudio_resetInstanceWaveBufState(inst);
+ if (targetSample >= inst->sampleCount) targetSample = inst->sampleCount > 0 ? inst->sampleCount - 1 : 0;
+ inst->waveBufs[0].data_pcm16 = inst->pcmData + (size_t) targetSample * inst->channelCount;
+ inst->waveBufs[0].nsamples = inst->sampleCount - targetSample;
+ inst->waveBufs[0].looping = inst->loop;
+ ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[0]);
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+
+ N3DSAudio_cancelAsyncFillLocked(audio, inst);
+ N3DSDebugLog_event("audio_seek", "stream instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample);
+ N3DSAudio_rebuildInstanceChannels(audio, inst);
+ if (!N3DSAudio_streamSeekSamples(inst, targetSample)) {
+ LightLock_Unlock(&audio->lock);
+ return;
+ }
+ N3DSAudio_primeStream(inst);
+ if (inst->paused) ndspChnSetPaused(inst->channelId, true);
+ LightLock_Unlock(&audio->lock);
+}
+
+static float N3DSAudio_getSoundLength(AudioSystem* base, int32_t soundOrInstance) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ N3DSSoundInstance* inst = NULL;
+ if (N3DSAudio_isInstanceId(soundOrInstance)) {
+ inst = N3DSAudio_findInstanceById(audio, soundOrInstance);
+ } else {
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) {
+ inst = &audio->instances[i];
+ break;
+ }
+ }
+ }
+ if (inst != NULL && inst->sampleRate > 0) {
+ float length = (float) inst->sampleCount / (float) inst->sampleRate;
+ LightLock_Unlock(&audio->lock);
+ return length;
+ }
+
+ if (soundOrInstance >= N3DS_AUDIO_STREAM_INDEX_BASE) {
+ int32_t slot = soundOrInstance - N3DS_AUDIO_STREAM_INDEX_BASE;
+ if (slot >= 0 && slot < N3DS_MAX_STREAMS && audio->streams[slot].active && audio->streams[slot].bcwav.sampleRate > 0) {
+ float length = (float) audio->streams[slot].bcwav.sampleCount / (float) audio->streams[slot].bcwav.sampleRate;
+ LightLock_Unlock(&audio->lock);
+ return length;
+ }
+ }
+
+ LightLock_Unlock(&audio->lock);
+ return 0.0f;
+}
+
+static void N3DSAudio_setMasterGain(AudioSystem* base, float gain) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ audio->masterGain = gain;
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active) N3DSAudio_applyMix(audio, &audio->instances[i]);
+ }
+ LightLock_Unlock(&audio->lock);
+}
+
+static void N3DSAudio_setChannelCount(MAYBE_UNUSED AudioSystem* base, MAYBE_UNUSED int32_t count) {}
+static void N3DSAudio_groupLoad(MAYBE_UNUSED AudioSystem* base, MAYBE_UNUSED int32_t groupIndex) {}
+static bool N3DSAudio_groupIsLoaded(MAYBE_UNUSED AudioSystem* base, MAYBE_UNUSED int32_t groupIndex) { return true; }
+
+static int32_t N3DSAudio_createStream(AudioSystem* base, const char* filename) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ if (filename == NULL || filename[0] == '\0') {
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ char* path = N3DSAudio_resolveAudioFilePath(audio, filename);
+ if (path == NULL) {
+ fprintf(stderr, "N3DSAudio: createStream failed to resolve %s\n", filename);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ repeat(N3DS_MAX_STREAMS, i) {
+ if (audio->streams[i].active && audio->streams[i].path != NULL && strcmp(audio->streams[i].path, path) == 0) {
+ audio->streams[i].refCount += 1u;
+ free(path);
+ LightLock_Unlock(&audio->lock);
+ return N3DS_AUDIO_STREAM_INDEX_BASE + (int32_t) i;
+ }
+ }
+
+ repeat(N3DS_MAX_STREAMS, i) {
+ if (!audio->streams[i].active) {
+ N3DSBcwav bcwav;
+ if (!N3DSAudio_parseBcwavFile(path, &bcwav)) {
+ free(path);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+ }
+
+ audio->streams[i].active = true;
+ audio->streams[i].path = path;
+ audio->streams[i].blobData = NULL;
+ audio->streams[i].blobSize = 0;
+ audio->streams[i].bcwav = bcwav;
+ audio->streams[i].gain = 1.0f;
+ audio->streams[i].pitch = 1.0f;
+ audio->streams[i].refCount = 1u;
+ LightLock_Unlock(&audio->lock);
+ return N3DS_AUDIO_STREAM_INDEX_BASE + (int32_t) i;
+ }
+ }
+ fprintf(stderr, "N3DSAudio: createStream exhausted slots for %s\n", path);
+ free(path);
+ LightLock_Unlock(&audio->lock);
+ return -1;
+}
+
+static bool N3DSAudio_destroyStream(AudioSystem* base, int32_t streamIndex) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ LightLock_Lock(&audio->lock);
+ int32_t slot = streamIndex - N3DS_AUDIO_STREAM_INDEX_BASE;
+ if (slot < 0 || slot >= N3DS_MAX_STREAMS || !audio->streams[slot].active) {
+ LightLock_Unlock(&audio->lock);
+ return false;
+ }
+
+ if (audio->streams[slot].refCount > 1u) {
+ audio->streams[slot].refCount -= 1u;
+ LightLock_Unlock(&audio->lock);
+ return true;
+ }
+
+ repeat(N3DS_MAX_SOUND_INSTANCES, i) {
+ if (audio->instances[i].active && audio->instances[i].soundIndex == streamIndex) {
+ N3DSAudio_releaseInstance(audio, &audio->instances[i]);
+ }
+ }
+
+ free(audio->streams[slot].blobData);
+ free(audio->streams[slot].path);
+ memset(&audio->streams[slot], 0, sizeof(audio->streams[slot]));
+ LightLock_Unlock(&audio->lock);
+ return true;
+}
+
+static AudioSystemVtable N3DSAudio_vtable = {
+ .init = N3DSAudio_init,
+ .destroy = N3DSAudio_destroy,
+ .update = N3DSAudio_update,
+ .playSound = N3DSAudio_playSound,
+ .stopSound = N3DSAudio_stopSound,
+ .stopAll = N3DSAudio_stopAll,
+ .isPlaying = N3DSAudio_isPlaying,
+ .pauseSound = N3DSAudio_pauseSound,
+ .resumeSound = N3DSAudio_resumeSound,
+ .pauseAll = N3DSAudio_pauseAll,
+ .resumeAll = N3DSAudio_resumeAll,
+ .setSoundGain = N3DSAudio_setSoundGain,
+ .getSoundGain = N3DSAudio_getSoundGain,
+ .setSoundPitch = N3DSAudio_setSoundPitch,
+ .getSoundPitch = N3DSAudio_getSoundPitch,
+ .getTrackPosition = N3DSAudio_getTrackPosition,
+ .setTrackPosition = N3DSAudio_setTrackPosition,
+ .getSoundLength = N3DSAudio_getSoundLength,
+ .setMasterGain = N3DSAudio_setMasterGain,
+ .setChannelCount = N3DSAudio_setChannelCount,
+ .groupLoad = N3DSAudio_groupLoad,
+ .groupIsLoaded = N3DSAudio_groupIsLoaded,
+ .createStream = N3DSAudio_createStream,
+ .destroyStream = N3DSAudio_destroyStream,
+ .prewarmRoom = N3DSAudio_prewarmRoom,
+};
+
+N3DSAudioSystem* N3DSAudioSystem_create(void) {
+ N3DSAudioSystem* audio = safeCalloc(1, sizeof(N3DSAudioSystem));
+ audio->base.vtable = &N3DSAudio_vtable;
+ audio->masterGain = 1.0f;
+ audio->lastResolveFailureSound = INT32_MIN;
+ return audio;
+}
+
+void N3DSAudioSystem_getCacheStats(
+ AudioSystem* base,
+ uint32_t* outCachedSounds,
+ uint32_t* outTotalSounds,
+ uint32_t* outCachedBytes,
+ uint32_t* outCacheLimitBytes
+) {
+ N3DSAudioSystem* audio = (N3DSAudioSystem*) base;
+ if (outCachedSounds != NULL) *outCachedSounds = 0;
+ if (outTotalSounds != NULL) *outTotalSounds = 0;
+ if (outCachedBytes != NULL) *outCachedBytes = 0;
+ if (outCacheLimitBytes != NULL) *outCacheLimitBytes = 0;
+ if (audio == NULL) return;
+
+ uint32_t cachedSounds = 0;
+ LightLock_Lock(&audio->lock);
+ if (audio->cachedSounds != NULL) {
+ repeat(audio->cachedSoundCount, i) {
+ if (audio->cachedSounds[i].available) cachedSounds++;
+ }
+ }
+ if (outCachedSounds != NULL) *outCachedSounds = cachedSounds;
+ if (outTotalSounds != NULL) *outTotalSounds = audio->cachedSoundCount;
+ if (outCachedBytes != NULL) *outCachedBytes = audio->cachedSoundBytes;
+ if (outCacheLimitBytes != NULL) *outCacheLimitBytes = audio->maxCachedSoundBytes;
+ LightLock_Unlock(&audio->lock);
+}
diff --git a/src/n3ds/n3ds_audio_system.h b/src/n3ds/n3ds_audio_system.h
new file mode 100644
index 00000000..81d6126c
--- /dev/null
+++ b/src/n3ds/n3ds_audio_system.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "../audio_system.h"
+
+typedef struct N3DSAudioSystem N3DSAudioSystem;
+
+N3DSAudioSystem* N3DSAudioSystem_create(void);
+void N3DSAudioSystem_getCacheStats(
+ AudioSystem* base,
+ uint32_t* outCachedSounds,
+ uint32_t* outTotalSounds,
+ uint32_t* outCachedBytes,
+ uint32_t* outCacheLimitBytes
+);
diff --git a/src/n3ds/n3ds_debug_log.c b/src/n3ds/n3ds_debug_log.c
new file mode 100644
index 00000000..ba5c2f1f
--- /dev/null
+++ b/src/n3ds/n3ds_debug_log.c
@@ -0,0 +1,120 @@
+#include "n3ds_debug_log.h"
+
+#if N3DS_DEBUG_BREADCRUMB_LOGGING
+
+#include <3ds.h>
+
+#include
+#include
+#include
+#include
+#include
+
+#define N3DS_DEBUG_HISTORY_LOG_PATH "sdmc:/3ds/cinnamon/crash_history.log"
+#define N3DS_DEBUG_MARKER_LOG_PATH "sdmc:/3ds/cinnamon/last_breadcrumb.txt"
+#define N3DS_DEBUG_MARKER_BUFFER_SIZE 256
+
+static FILE* gN3DSDebugHistoryLog = NULL;
+static FILE* gN3DSDebugMarkerLog = NULL;
+static u64 gN3DSDebugStartTick = 0;
+static uint32_t gN3DSDebugEventCounter = 0;
+static char gN3DSDebugLastMarker[N3DS_DEBUG_MARKER_BUFFER_SIZE];
+
+static double N3DSDebugLog_ticksToMs(u64 ticks) {
+ return (double) ticks * 1000.0 / (double) SYSCLOCK_ARM11;
+}
+
+static void N3DSDebugLog_openIfNeeded(void) {
+ if (gN3DSDebugStartTick == 0) gN3DSDebugStartTick = svcGetSystemTick();
+
+ mkdir("sdmc:/3ds", 0777);
+ mkdir("sdmc:/3ds/cinnamon", 0777);
+
+ if (gN3DSDebugHistoryLog == NULL) {
+ gN3DSDebugHistoryLog = fopen(N3DS_DEBUG_HISTORY_LOG_PATH, "a");
+ if (gN3DSDebugHistoryLog != NULL) {
+ setvbuf(gN3DSDebugHistoryLog, NULL, _IOLBF, 0);
+ }
+ }
+
+ if (gN3DSDebugMarkerLog == NULL) {
+ gN3DSDebugMarkerLog = fopen(N3DS_DEBUG_MARKER_LOG_PATH, "w");
+ if (gN3DSDebugMarkerLog != NULL) {
+ setvbuf(gN3DSDebugMarkerLog, NULL, _IONBF, 0);
+ }
+ }
+}
+
+void N3DSDebugLog_init(void) {
+ N3DSDebugLog_openIfNeeded();
+ N3DSDebugLog_event("boot", "debug log initialized");
+}
+
+void N3DSDebugLog_close(void) {
+ if (gN3DSDebugMarkerLog != NULL) {
+ fclose(gN3DSDebugMarkerLog);
+ gN3DSDebugMarkerLog = NULL;
+ }
+ if (gN3DSDebugHistoryLog != NULL) {
+ fclose(gN3DSDebugHistoryLog);
+ gN3DSDebugHistoryLog = NULL;
+ }
+}
+
+void N3DSDebugLog_event(const char* tag, const char* fmt, ...) {
+ N3DSDebugLog_openIfNeeded();
+ if (gN3DSDebugHistoryLog == NULL) return;
+
+ char message[160];
+ va_list args;
+ va_start(args, fmt);
+ vsnprintf(message, sizeof(message), fmt != NULL ? fmt : "", args);
+ va_end(args);
+
+ double elapsedMs = N3DSDebugLog_ticksToMs(svcGetSystemTick() - gN3DSDebugStartTick);
+ fprintf(
+ gN3DSDebugHistoryLog,
+ "[%10.2f ms] #%06lu %-10s %s\n",
+ elapsedMs,
+ (unsigned long) ++gN3DSDebugEventCounter,
+ tag != NULL ? tag : "event",
+ message
+ );
+ fflush(gN3DSDebugHistoryLog);
+}
+
+void N3DSDebugLog_setMarker(
+ const char* stage,
+ uint32_t frame,
+ const char* roomName,
+ int32_t detailA,
+ int32_t detailB
+) {
+ N3DSDebugLog_openIfNeeded();
+ if (gN3DSDebugMarkerLog == NULL) return;
+
+ char line[N3DS_DEBUG_MARKER_BUFFER_SIZE];
+ double elapsedMs = N3DSDebugLog_ticksToMs(svcGetSystemTick() - gN3DSDebugStartTick);
+ snprintf(
+ line,
+ sizeof(line),
+ "t=%10.2fms frame=%lu stage=%s room=%s a=%ld b=%ld",
+ elapsedMs,
+ (unsigned long) frame,
+ stage != NULL ? stage : "",
+ roomName != NULL ? roomName : "(none)",
+ (long) detailA,
+ (long) detailB
+ );
+
+ if (strncmp(line, gN3DSDebugLastMarker, sizeof(gN3DSDebugLastMarker)) == 0) {
+ return;
+ }
+ snprintf(gN3DSDebugLastMarker, sizeof(gN3DSDebugLastMarker), "%s", line);
+
+ rewind(gN3DSDebugMarkerLog);
+ fprintf(gN3DSDebugMarkerLog, "%-240s\n", line);
+ fflush(gN3DSDebugMarkerLog);
+}
+
+#endif
diff --git a/src/n3ds/n3ds_debug_log.h b/src/n3ds/n3ds_debug_log.h
new file mode 100644
index 00000000..914df4e6
--- /dev/null
+++ b/src/n3ds/n3ds_debug_log.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include
+
+#ifndef N3DS_DEBUG_BREADCRUMB_LOGGING
+#define N3DS_DEBUG_BREADCRUMB_LOGGING 0
+#endif
+
+#if N3DS_DEBUG_BREADCRUMB_LOGGING
+void N3DSDebugLog_init(void);
+void N3DSDebugLog_close(void);
+void N3DSDebugLog_event(const char* tag, const char* fmt, ...);
+void N3DSDebugLog_setMarker(
+ const char* stage,
+ uint32_t frame,
+ const char* roomName,
+ int32_t detailA,
+ int32_t detailB
+);
+#else
+#define N3DSDebugLog_init() ((void) 0)
+#define N3DSDebugLog_close() ((void) 0)
+#define N3DSDebugLog_event(...) ((void) 0)
+#define N3DSDebugLog_setMarker(...) ((void) 0)
+#endif
diff --git a/src/n3ds/n3ds_file_system.c b/src/n3ds/n3ds_file_system.c
new file mode 100644
index 00000000..c3f6976a
--- /dev/null
+++ b/src/n3ds/n3ds_file_system.c
@@ -0,0 +1,329 @@
+#include "n3ds_file_system.h"
+
+#include "../utils.h"
+
+#include <3ds.h>
+#include
+#include
+#include
+#include
+#include
+
+#define N3DS_ENABLE_LOGGING 0
+#define N3DS_FILE_BUFFER_SIZE (32u * 1024u)
+
+typedef struct N3DSFileSystemResolvedPathEntry {
+ char* key;
+ char* value;
+} N3DSFileSystemResolvedPathEntry;
+
+static FILE* gN3DSSdIoLogFile = NULL;
+
+static double N3DSFileSystem_ticksToMs(u64 ticks) {
+ return (double) ticks * 1000.0 / (double) SYSCLOCK_ARM11;
+}
+
+static bool N3DSFileSystem_isSdmcPath(const char* path) {
+ return path != NULL && strncmp(path, "sdmc:/", 6) == 0;
+}
+
+static void N3DSFileSystem_openSdIoLog(void) {
+#if !N3DS_ENABLE_LOGGING
+ return;
+#else
+ if (gN3DSSdIoLogFile != NULL) return;
+ gN3DSSdIoLogFile = fopen("sdmc:/3ds/cinnamon/sd_io.log", "a");
+ if (gN3DSSdIoLogFile != NULL) {
+ setvbuf(gN3DSSdIoLogFile, NULL, _IOLBF, 0);
+ }
+#endif
+}
+
+static void N3DSFileSystem_logSdIo(
+ const char* op,
+ const char* path,
+ double elapsedMs,
+ long sizeBytes,
+ bool ok
+) {
+#if !N3DS_ENABLE_LOGGING
+ (void) op;
+ (void) path;
+ (void) elapsedMs;
+ (void) sizeBytes;
+ (void) ok;
+ return;
+#else
+ if (path == NULL || !N3DSFileSystem_isSdmcPath(path)) return;
+ if (elapsedMs < 0.25 && ok) return;
+
+ N3DSFileSystem_openSdIoLog();
+ fprintf(
+ stderr,
+ "N3DS SDIO: %-10s %7.2f ms %s size=%ld ok=%d\n",
+ op != NULL ? op : "",
+ elapsedMs,
+ path,
+ sizeBytes,
+ ok ? 1 : 0
+ );
+ if (gN3DSSdIoLogFile != NULL) {
+ fprintf(
+ gN3DSSdIoLogFile,
+ "N3DS SDIO: %-10s %7.2f ms %s size=%ld ok=%d\n",
+ op != NULL ? op : "",
+ elapsedMs,
+ path,
+ sizeBytes,
+ ok ? 1 : 0
+ );
+ fflush(gN3DSSdIoLogFile);
+ }
+#endif
+}
+
+static bool N3DSFileSystem_hasScheme(const char* path) {
+ return path != NULL && strstr(path, ":/") != NULL;
+}
+
+static void N3DSFileSystem_configureFileBuffer(FILE* file) {
+ if (file == NULL) return;
+ setvbuf(file, NULL, _IOFBF, N3DS_FILE_BUFFER_SIZE);
+}
+
+static bool N3DSFileSystem_pathExists(const char* path) {
+ u64 startTick = svcGetSystemTick();
+ struct stat st;
+ bool ok = path != NULL && stat(path, &st) == 0;
+ N3DSFileSystem_logSdIo("stat", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, ok);
+ return ok;
+}
+
+static char* N3DSFileSystem_join(const char* base, const char* relativePath) {
+ if (relativePath == NULL) return NULL;
+ if (N3DSFileSystem_hasScheme(relativePath)) return safeStrdup(relativePath);
+
+ size_t baseLen = strlen(base);
+ size_t relLen = strlen(relativePath);
+ bool needSlash = baseLen > 0 && base[baseLen - 1] != '/' && relLen > 0 && relativePath[0] != '/';
+ char* result = safeMalloc(baseLen + relLen + (needSlash ? 2 : 1));
+ memcpy(result, base, baseLen);
+ size_t cursor = baseLen;
+ if (needSlash) result[cursor++] = '/';
+ memcpy(result + cursor, relativePath, relLen);
+ result[cursor + relLen] = '\0';
+ return result;
+}
+
+static char* N3DSFileSystem_getCachedResolvedPath(N3DSFileSystem* n3ds, const char* relativePath) {
+ if (n3ds == NULL || relativePath == NULL) return NULL;
+ ptrdiff_t idx = shgeti(n3ds->resolvedPathCache, relativePath);
+ if (idx < 0) return NULL;
+ return safeStrdup(n3ds->resolvedPathCache[idx].value);
+}
+
+static void N3DSFileSystem_setCachedResolvedPath(N3DSFileSystem* n3ds, const char* relativePath, const char* resolvedPath) {
+ if (n3ds == NULL || relativePath == NULL || resolvedPath == NULL) return;
+ ptrdiff_t idx = shgeti(n3ds->resolvedPathCache, relativePath);
+ if (idx >= 0) {
+ free(n3ds->resolvedPathCache[idx].value);
+ n3ds->resolvedPathCache[idx].value = safeStrdup(resolvedPath);
+ return;
+ }
+
+ shput(n3ds->resolvedPathCache, relativePath, safeStrdup(resolvedPath));
+}
+
+static char* N3DSFileSystem_resolvePath(FileSystem* fs, const char* relativePath) {
+ N3DSFileSystem* n3ds = (N3DSFileSystem*) fs;
+ if (relativePath == NULL) return NULL;
+ if (N3DSFileSystem_hasScheme(relativePath)) return safeStrdup(relativePath);
+
+ char* cachedPath = N3DSFileSystem_getCachedResolvedPath(n3ds, relativePath);
+ if (cachedPath != NULL) return cachedPath;
+
+ char* romfsPath = N3DSFileSystem_join(n3ds->romfsBasePath, relativePath);
+ if (N3DSFileSystem_pathExists(romfsPath)) {
+ N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, romfsPath);
+ return romfsPath;
+ }
+
+ char* savePath = N3DSFileSystem_join(n3ds->saveBasePath, relativePath);
+ N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, savePath);
+ free(romfsPath);
+ return savePath;
+}
+
+static bool N3DSFileSystem_fileExists(FileSystem* fs, const char* relativePath) {
+ u64 startTick = svcGetSystemTick();
+ char* path = N3DSFileSystem_resolvePath(fs, relativePath);
+ struct stat st;
+ bool ok = path != NULL && stat(path, &st) == 0;
+ N3DSFileSystem_logSdIo("exists", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, ok);
+ free(path);
+ return ok;
+}
+
+static char* N3DSFileSystem_readFileText(FileSystem* fs, const char* relativePath) {
+ u64 startTick = svcGetSystemTick();
+ char* path = N3DSFileSystem_resolvePath(fs, relativePath);
+ if (path == NULL) return NULL;
+
+ FILE* file = fopen(path, "rb");
+ if (file == NULL) {
+ N3DSFileSystem_logSdIo("readText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, false);
+ free(path);
+ return NULL;
+ }
+ N3DSFileSystem_configureFileBuffer(file);
+
+ fseek(file, 0, SEEK_END);
+ long size = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ if (size < 0) {
+ fclose(file);
+ return NULL;
+ }
+
+ char* text = safeMalloc((size_t) size + 1);
+ size_t bytesRead = fread(text, 1, (size_t) size, file);
+ fclose(file);
+ text[bytesRead] = '\0';
+ N3DSFileSystem_logSdIo("readText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), (long) bytesRead, true);
+ free(path);
+ return text;
+}
+
+static bool N3DSFileSystem_writeFileText(FileSystem* fs, const char* relativePath, const char* contents) {
+ N3DSFileSystem* n3ds = (N3DSFileSystem*) fs;
+ u64 startTick = svcGetSystemTick();
+ char* path = N3DSFileSystem_join(n3ds->saveBasePath, relativePath);
+ if (path == NULL) return false;
+
+ FILE* file = fopen(path, "wb");
+ if (file == NULL) {
+ N3DSFileSystem_logSdIo("writeText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, false);
+ free(path);
+ return false;
+ }
+ N3DSFileSystem_configureFileBuffer(file);
+
+ size_t length = strlen(contents);
+ bool ok = fwrite(contents, 1, length, file) == length;
+ fclose(file);
+ N3DSFileSystem_logSdIo("writeText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), (long) length, ok);
+ if (ok) {
+ N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, path);
+ }
+ free(path);
+ return ok;
+}
+
+static bool N3DSFileSystem_deleteFile(FileSystem* fs, const char* relativePath) {
+ N3DSFileSystem* n3ds = (N3DSFileSystem*) fs;
+ u64 startTick = svcGetSystemTick();
+ char* path = N3DSFileSystem_join(n3ds->saveBasePath, relativePath);
+ if (path == NULL) return false;
+ int rc = remove(path);
+ N3DSFileSystem_logSdIo("delete", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, rc == 0);
+ if (rc == 0) {
+ char* romfsPath = N3DSFileSystem_join(n3ds->romfsBasePath, relativePath);
+ N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, romfsPath);
+ free(romfsPath);
+ }
+ free(path);
+ return rc == 0;
+}
+
+static bool N3DSFileSystem_readFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) {
+ u64 startTick = svcGetSystemTick();
+ char* path = N3DSFileSystem_resolvePath(fs, relativePath);
+ if (path == NULL) return false;
+
+ FILE* file = fopen(path, "rb");
+ if (file == NULL) {
+ N3DSFileSystem_logSdIo("readBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, false);
+ free(path);
+ return false;
+ }
+ N3DSFileSystem_configureFileBuffer(file);
+
+ fseek(file, 0, SEEK_END);
+ long size = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ if (size < 0) {
+ fclose(file);
+ return false;
+ }
+
+ uint8_t* data = safeMalloc((size_t) size);
+ size_t bytesRead = fread(data, 1, (size_t) size, file);
+ fclose(file);
+
+ *outData = data;
+ *outSize = (int32_t) bytesRead;
+ N3DSFileSystem_logSdIo("readBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), (long) bytesRead, true);
+ free(path);
+ return true;
+}
+
+static bool N3DSFileSystem_writeFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) {
+ N3DSFileSystem* n3ds = (N3DSFileSystem*) fs;
+ u64 startTick = svcGetSystemTick();
+ char* path = N3DSFileSystem_join(n3ds->saveBasePath, relativePath);
+ if (path == NULL) return false;
+
+ FILE* file = fopen(path, "wb");
+ if (file == NULL) {
+ N3DSFileSystem_logSdIo("writeBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), size, false);
+ free(path);
+ return false;
+ }
+ N3DSFileSystem_configureFileBuffer(file);
+
+ bool ok = fwrite(data, 1, (size_t) size, file) == (size_t) size;
+ fclose(file);
+ N3DSFileSystem_logSdIo("writeBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), size, ok);
+ if (ok) {
+ N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, path);
+ }
+ free(path);
+ return ok;
+}
+
+static FileSystemVtable N3DSFileSystem_vtable = {
+ .resolvePath = N3DSFileSystem_resolvePath,
+ .fileExists = N3DSFileSystem_fileExists,
+ .readFileText = N3DSFileSystem_readFileText,
+ .writeFileText = N3DSFileSystem_writeFileText,
+ .deleteFile = N3DSFileSystem_deleteFile,
+ .readFileBinary = N3DSFileSystem_readFileBinary,
+ .writeFileBinary = N3DSFileSystem_writeFileBinary,
+};
+
+N3DSFileSystem* N3DSFileSystem_create(const char* romfsBasePath, const char* saveBasePath) {
+ N3DSFileSystem* fs = safeCalloc(1, sizeof(N3DSFileSystem));
+ fs->base.vtable = &N3DSFileSystem_vtable;
+ fs->romfsBasePath = safeStrdup(romfsBasePath != NULL ? romfsBasePath : "romfs:/");
+ fs->saveBasePath = safeStrdup(saveBasePath != NULL ? saveBasePath : "sdmc:/3ds/cinnamon/");
+ fs->resolvedPathCache = NULL;
+ sh_new_strdup(fs->resolvedPathCache);
+ return fs;
+}
+
+void N3DSFileSystem_destroy(N3DSFileSystem* fs) {
+ if (fs == NULL) return;
+#if N3DS_ENABLE_LOGGING
+ if (gN3DSSdIoLogFile != NULL) {
+ fclose(gN3DSSdIoLogFile);
+ gN3DSSdIoLogFile = NULL;
+ }
+#endif
+ free(fs->romfsBasePath);
+ free(fs->saveBasePath);
+ repeat(shlen(fs->resolvedPathCache), i) {
+ free(fs->resolvedPathCache[i].value);
+ }
+ shfree(fs->resolvedPathCache);
+ free(fs);
+}
diff --git a/src/n3ds/n3ds_file_system.h b/src/n3ds/n3ds_file_system.h
new file mode 100644
index 00000000..26767247
--- /dev/null
+++ b/src/n3ds/n3ds_file_system.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "../file_system.h"
+
+typedef struct N3DSFileSystemResolvedPathEntry N3DSFileSystemResolvedPathEntry;
+
+typedef struct {
+ FileSystem base;
+ char* romfsBasePath;
+ char* saveBasePath;
+ N3DSFileSystemResolvedPathEntry* resolvedPathCache;
+} N3DSFileSystem;
+
+N3DSFileSystem* N3DSFileSystem_create(const char* romfsBasePath, const char* saveBasePath);
+void N3DSFileSystem_destroy(N3DSFileSystem* fs);
diff --git a/src/n3ds/n3ds_platform_config.h b/src/n3ds/n3ds_platform_config.h
new file mode 100644
index 00000000..85613763
--- /dev/null
+++ b/src/n3ds/n3ds_platform_config.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#define N3DS_FORCE_OLD3DS_MODE 0
\ No newline at end of file
diff --git a/src/n3ds/n3ds_renderer.c b/src/n3ds/n3ds_renderer.c
new file mode 100644
index 00000000..7ceba748
--- /dev/null
+++ b/src/n3ds/n3ds_renderer.c
@@ -0,0 +1,5530 @@
+#include "n3ds_renderer.h"
+#include "n3ds_platform_config.h"
+
+#include "../runner.h"
+#include "../text_utils.h"
+#include "../utils.h"
+
+#include <3ds.h>
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#define N3DS_ENABLE_LOGGING 0
+#define N3DS_ENABLE_DIRECT_ASSETS 1
+
+#if !N3DS_ENABLE_LOGGING
+#define fprintf(...) ((int) 0)
+#endif
+
+#define N3DS_ATLAS_MAGIC 0x5441334Eu /* N3AT */
+#define N3DS_ATLAS_VERSION_RAW 1u
+#define N3DS_ATLAS_VERSION_T3X 2u
+#define N3DS_ATLAS_VERSION_FRAGMENTED 3u
+#define N3DS_ATLAS_VERSION_FRAGMENTED_TILES 4u
+#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS 5u
+#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT 6u
+#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT 7u
+#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED 8u
+#define N3DS_DIRECT_ASSET_MAGIC 0x3152444Eu /* NDR1 */
+#define N3DS_DIRECT_ASSET_VERSION 1u
+#define N3DS_ROOM_MANIFEST_MAGIC 0x4D52334Eu /* N3RM */
+#define N3DS_ROOM_MANIFEST_VERSION 1u
+#define N3DS_DIRECT_ASSET_ENTRY_SIZE 16u
+#define N3DS_TOP_WIDTH 400
+#define N3DS_TOP_HEIGHT 240
+#define N3DS_BOTTOM_WIDTH 320
+#define N3DS_BOTTOM_HEIGHT 240
+#define N3DS_SDMC_ASSET_BASE "sdmc:/3ds/cinnamon/gfx"
+#define N3DS_ROMFS_ASSET_BASE "romfs:/gfx"
+#define N3DS_RENDERER_FILE_BUFFER_SIZE (32u * 1024u)
+#define N3DS_TILE_LAYER_CHUNK_SIZE 256u
+#define N3DS_TILE_LAYER_CHUNK_TEXTURE_BYTES (N3DS_TILE_LAYER_CHUNK_SIZE * N3DS_TILE_LAYER_CHUNK_SIZE * 2u)
+#define N3DS_TILE_LAYER_CHUNK_VRAM_BUDGET (512u * 1024u)
+#define N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS 16u
+#define N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS 32u
+#define N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS_ETC1A4 40u
+#define N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS_ETC1A4 80u
+#define N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS 12u
+#define N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS 32u
+#define N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS_ETC1A4 20u
+#define N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS_ETC1A4 48u
+#define N3DS_PREWARM_BLOB_BYTES_OLD3DS (8u * 1024u * 1024u)
+#define N3DS_PREWARM_BLOB_BYTES_NEW3DS (12u * 1024u * 1024u)
+#define N3DS_RESIDENT_ATLAS_VRAM_BUDGET_OLD3DS (2560u * 1024u)
+#define N3DS_RESIDENT_ATLAS_VRAM_BUDGET_NEW3DS (5120u * 1024u)
+#define N3DS_MAX_CACHED_T3X_BYTES_OLD3DS (16u * 1024u * 1024u)
+#define N3DS_MAX_CACHED_T3X_BYTES_NEW3DS (24u * 1024u * 1024u)
+#define N3DS_MAX_CACHED_DIRECT_T3X_BYTES_OLD3DS (4u * 1024u * 1024u)
+#define N3DS_MAX_CACHED_DIRECT_T3X_BYTES_NEW3DS (8u * 1024u * 1024u)
+#define N3DS_DIRECT_ASSET_VRAM_BUDGET_OLD3DS (1024u * 1024u)
+#define N3DS_DIRECT_ASSET_VRAM_BUDGET_NEW3DS (3072u * 1024u)
+// battle screen offset
+#define N3DS_TOP_BATTLE_SCENE_Y_OFFSET 200.0f
+#define N3DS_TOP_BATTLE_ENEMY_Y_OFFSET 200.0f
+#define N3DS_C2D_FLUSH_DRAW_BUDGET 192u
+#define N3DS_PERF_LOG_INTERVAL_FRAMES 30u
+#define N3DS_ATLAS_TRACE_LOG_PATH "sdmc:/3ds/cinnamon/atlas_trace.log"
+
+typedef struct {
+ uint16_t width;
+ uint16_t height;
+ uint32_t textureFormat;
+ uint32_t dataOffset;
+ uint32_t dataSize;
+} N3DSAtlasPageInfo;
+
+typedef enum {
+ N3DS_TEXFMT_RGBA5551 = 0,
+ N3DS_TEXFMT_ETC1A4 = 1,
+ N3DS_TEXFMT_INDEXED8 = 2,
+ N3DS_TEXFMT_HYBRID = 3,
+ N3DS_TEXFMT_L4 = 4,
+ N3DS_TEXFMT_LA4 = 5,
+} N3DSTextureFormat;
+
+typedef struct {
+ uint16_t atlasId;
+ uint16_t x;
+ uint16_t y;
+ uint16_t width;
+ uint16_t height;
+} N3DSAtlasItem;
+
+typedef struct {
+ uint16_t width;
+ uint16_t height;
+ uint32_t fragmentStart;
+ uint16_t fragmentCount;
+} N3DSAtlasItemV3;
+
+typedef struct {
+ uint16_t atlasId;
+ uint16_t x;
+ uint16_t y;
+ uint16_t width;
+ uint16_t height;
+ uint16_t sourceX;
+ uint16_t sourceY;
+} N3DSAtlasFragment;
+
+typedef struct {
+ int16_t bgDef;
+ uint16_t srcX;
+ uint16_t srcY;
+ uint16_t srcW;
+ uint16_t srcH;
+ uint16_t atlasId;
+ uint16_t x;
+ uint16_t y;
+ uint16_t width;
+ uint16_t height;
+ uint32_t fragmentStart;
+ uint16_t fragmentCount;
+} N3DSTileAtlasEntry;
+
+typedef struct {
+ int16_t bgDef;
+ uint16_t srcX;
+ uint16_t srcY;
+ uint16_t srcW;
+ uint16_t srcH;
+} N3DSTileLookupKey;
+
+typedef struct {
+ N3DSTileLookupKey key;
+ uint32_t value;
+} N3DSTileEntryMap;
+
+typedef struct {
+ C2D_SpriteSheet sheet;
+ C2D_Image image;
+ uint8_t* blobData;
+ uint32_t blobSize;
+ uint32_t blobLastUsedStamp;
+ bool ready;
+ bool failed;
+ bool pinned;
+ uint32_t vramBytes;
+ uint32_t lastUsedStamp;
+ uint32_t lastUsedFrame;
+} N3DSDirectTextureAsset;
+
+typedef struct {
+ uint32_t frameCount;
+ uint32_t sheetFrameCount;
+ bool sheetLoadAttempted;
+ bool useFrameFallback;
+ C2D_Image* sheetFrameImages;
+ N3DSDirectTextureAsset sheetAsset;
+ N3DSDirectTextureAsset* frameAssets;
+} N3DSDirectSpriteAsset;
+
+typedef struct {
+ bool allocated;
+ int32_t x;
+ int32_t y;
+ uint16_t width;
+ uint16_t height;
+ C3D_Tex texture;
+ C3D_RenderTarget* target;
+ Tex3DS_SubTexture subtex;
+ C2D_Image image;
+} N3DSTileLayerChunk;
+
+typedef struct {
+ bool used;
+ uint32_t chunkCount;
+ uint32_t vramBytes;
+ N3DSTileLayerChunk* chunks;
+} N3DSTileLayerChunkCache;
+
+typedef struct {
+ C3D_Tex texture;
+ Tex3DS_Texture t3x;
+ uint8_t* t3xData;
+ uint32_t t3xSize;
+ uint32_t blobLastUsedStamp;
+ bool ready;
+ bool pinned;
+ uint16_t width;
+ uint16_t height;
+ uint32_t textureFormat;
+ uint32_t dataOffset;
+ uint32_t dataSize;
+ uint32_t lastUsedStamp;
+ uint32_t lastUsedFrame;
+} N3DSLoadedAtlasPage;
+
+typedef struct {
+ bool used;
+ int32_t ownerSpriteIndex;
+ uint16_t logicalWidth;
+ uint16_t logicalHeight;
+ C3D_Tex texture;
+ Tex3DS_SubTexture subtex;
+ C2D_Image image;
+} N3DSDynamicCaptureTPAG;
+
+typedef struct {
+ float localX;
+ float localY;
+ uint16_t sourceX;
+ uint16_t sourceY;
+ uint16_t sourceWidth;
+ uint16_t sourceHeight;
+} N3DSCachedTextGlyph;
+
+typedef struct {
+ char* text;
+ int32_t textCapacity;
+ int32_t fontIndex;
+ int32_t drawHalign;
+ int32_t drawValign;
+ int32_t glyphCount;
+ int32_t glyphCapacity;
+ N3DSCachedTextGlyph* glyphs;
+ float appendCursorX;
+ float appendCursorY;
+ uint16_t appendPrevCodepoint;
+ bool appendAtLineStart;
+} N3DSCachedTextLayout;
+
+typedef struct {
+ char* key;
+ char* value;
+} N3DSResolvedAssetPathEntry;
+
+typedef struct {
+ uint32_t dataOffset;
+ uint32_t dataSize;
+} N3DSPackedDirectAssetEntry;
+
+typedef struct {
+ char* key;
+ N3DSPackedDirectAssetEntry value;
+} N3DSPackedDirectAssetMapEntry;
+
+typedef struct {
+ uint32_t roomIndex;
+ uint32_t pageStart;
+ uint32_t pageCount;
+ uint32_t directSpriteStart;
+ uint32_t directSpriteCount;
+ uint32_t directBackgroundStart;
+ uint32_t directBackgroundCount;
+} N3DSRoomManifestEntry;
+
+typedef struct {
+ uint16_t pageIndex;
+ uint8_t flags;
+ uint8_t reserved;
+} N3DSRoomManifestPageRef;
+
+typedef struct {
+ Renderer base;
+ C3D_RenderTarget* topTarget;
+ C3D_RenderTarget* bottomTarget;
+ N3DSLoadedAtlasPage* atlasPages;
+ N3DSAtlasItem* atlasItems;
+ N3DSAtlasItemV3* atlasItemsV3;
+ N3DSAtlasFragment* atlasFragments;
+ N3DSTileAtlasEntry* tileEntries;
+ N3DSTileEntryMap* tileEntryMap;
+ N3DSTileLayerChunkCache* tileLayerChunkCaches;
+ N3DSDirectSpriteAsset* directSpriteAssets;
+ N3DSDirectTextureAsset* directBackgroundAssets;
+ N3DSDirectTextureAsset* directFontAssets;
+ int32_t* tpagToSpriteIndex;
+ int32_t* tpagToSpriteFrameIndex;
+ int32_t* tpagToBackgroundIndex;
+ uint32_t atlasPageCount;
+ uint32_t atlasItemCount;
+ uint32_t atlasFragmentCount;
+ uint32_t tileEntryCount;
+ uint32_t directSpriteAssetCount;
+ uint32_t directBackgroundAssetCount;
+ uint32_t directFontAssetCount;
+ uint16_t atlasVersion;
+ uint32_t atlasTextureFormat;
+ float frameScaleX;
+ float frameScaleY;
+ float frameOffsetX;
+ float frameOffsetY;
+ float portOffsetX;
+ float portOffsetY;
+ int32_t viewX;
+ int32_t viewY;
+ float viewScaleX;
+ float viewScaleY;
+ bool blendEnabled;
+ int32_t blendEquation;
+ int32_t blendSrcFactor;
+ int32_t blendDstFactor;
+ bool alphaTestEnabled;
+ uint8_t alphaTestRef;
+ uint32_t clearColor;
+ float clearAlpha;
+ uint32_t atlasUseCounter;
+ uint32_t blobUseCounter;
+ uint32_t residentAtlasPageCount;
+ uint32_t residentAtlasVRAMBytes;
+ uint32_t residentAtlasPageLimit;
+ uint32_t residentAtlasVRAMLimitBytes;
+ uint32_t directAssetUseCounter;
+ uint32_t directBlobUseCounter;
+ uint32_t residentDirectAssetVRAMBytes;
+ uint32_t residentDirectAssetVRAMLimitBytes;
+ uint32_t prewarmGpuPageBudget;
+ uint32_t cachedT3xByteLimit;
+ uint32_t cachedT3xBytes;
+ uint32_t cachedDirectT3xByteLimit;
+ uint32_t cachedDirectT3xBytes;
+ uint32_t tileLayerChunkVRAMBytes;
+ uint32_t frameBlobReads;
+ uint32_t frameDirectBlobReads;
+ uint32_t framePageImports;
+ uint32_t frameImportFailures;
+ uint32_t frameImportEvictions;
+ uint32_t frameFragmentDraws;
+ uint32_t frameSpriteDrawCalls;
+ uint32_t frameSpritePartDrawCalls;
+ uint32_t frameDirectSpriteHits;
+ uint32_t frameDirectAssetLoads;
+ uint32_t frameTextureSwitches;
+ uint32_t frameTextGlyphDraws;
+ uint32_t frameTextTenthsMs;
+ uint32_t pendingC2DDraws;
+ bool atlasLoaded;
+ char startupError[160];
+ int32_t lastDirectTPAGIndex;
+ C2D_Image* lastDirectTPAGImage;
+ uint32_t perfWindowFrames;
+ uint32_t perfWindowBlobReads;
+ uint32_t perfWindowPageImports;
+ uint32_t perfWindowImportFailures;
+ uint32_t perfWindowImportEvictions;
+ uint32_t perfWindowFragmentDraws;
+ uint32_t perfWindowSpriteDrawCalls;
+ uint32_t perfWindowSpritePartDrawCalls;
+ uint32_t frameSequence;
+ const Room* lastPrewarmedRoom;
+ N3DSCachedTextLayout cachedTextLayout;
+ N3DSResolvedAssetPathEntry* resolvedAssetPathCache;
+ N3DSPackedDirectAssetMapEntry* packedDirectAssetMap;
+ N3DSRoomManifestEntry* roomManifestEntries;
+ N3DSRoomManifestPageRef* roomManifestPageRefs;
+ N3DSDynamicCaptureTPAG* dynamicCaptureTPAGs;
+ uint8_t* atlasTraceMask;
+ FILE* atlasTraceFile;
+ FILE* packedAtlasFile;
+ FILE* packedDirectAssetFile;
+ uint32_t roomManifestEntryCount;
+ uint32_t roomManifestPageRefCount;
+ uint32_t roomManifestRoomCount;
+ uint32_t baseTPAGCount;
+ uint32_t dynamicCaptureTPAGCount;
+ bool bottomScreenGuiActive;
+ bool topScreenGuiActive;
+ bool topScreenGui2xActive;
+ bool topScreenBattleViewActive;
+ bool textLinearFilterActive;
+ uint8_t activeSceneTarget;
+ const C3D_Tex* lastDrawTexture;
+ float savedFrameScaleX;
+ float savedFrameScaleY;
+ float savedFrameOffsetX;
+ float savedFrameOffsetY;
+ float savedPortOffsetX;
+ float savedPortOffsetY;
+ int32_t savedViewX;
+ int32_t savedViewY;
+ float savedViewScaleX;
+ float savedViewScaleY;
+ bool isNew3DS;
+ bool pendingOld3DSAtlasFlush;
+} N3DSRenderer;
+
+enum {
+ N3DS_SCENE_TARGET_NONE = 0,
+ N3DS_SCENE_TARGET_TOP = 1,
+ N3DS_SCENE_TARGET_BOTTOM = 2,
+};
+
+static void N3DSRenderer_drawSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha);
+static void N3DSRenderer_drawSpritePart(Renderer* base, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha);
+static void N3DSRenderer_drawTile(Renderer* base, RoomTile* tile, float offsetX, float offsetY);
+static void N3DSRenderer_drawTiled(Renderer* base, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha);
+static void N3DSRenderer_drawTiledPart(Renderer* base, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha);
+static bool N3DSRenderer_getPageIndexForTPAG(N3DSRenderer* renderer, int32_t tpagIndex, uint32_t* outPageIndex);
+static N3DSTileAtlasEntry* N3DSRenderer_findTileEntryByKey(N3DSRenderer* renderer, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, uint32_t* outEntryIndex);
+static N3DSTileAtlasEntry* N3DSRenderer_findTileEntry(N3DSRenderer* renderer, RoomTile* tile);
+static void N3DSRenderer_prewarmPage(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex);
+static bool N3DSRenderer_prewarmPageBlobOnly(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex, uint32_t* remainingBlobBytes);
+static void N3DSRenderer_prewarmTPAGBlobOnly(N3DSRenderer* renderer, bool* seenPages, int32_t tpagIndex, uint32_t* remainingBlobBytes);
+static void N3DSRenderer_prewarmTileEntryBlobOnly(N3DSRenderer* renderer, bool* seenPages, const N3DSTileAtlasEntry* entry, uint32_t* remainingBlobBytes);
+static void N3DSRenderer_prewarmRoomBlobCache(N3DSRenderer* renderer, Runner* runner);
+static void N3DSRenderer_preloadFontPages(N3DSRenderer* renderer);
+static bool N3DSRenderer_ensurePageBlobLoaded(N3DSRenderer* renderer, uint32_t pageIndex);
+static void N3DSRenderer_prewarmRoom(Renderer* base, Runner* runner);
+static void N3DSRenderer_freeCachedTextLayout(N3DSCachedTextLayout* layout);
+static void N3DSRenderer_appendCachedTextLayoutSuffix(N3DSCachedTextLayout* layout, Font* font, const char* suffix, int32_t suffixLen);
+static const N3DSCachedTextLayout* N3DSRenderer_getCachedTextLayout(N3DSRenderer* renderer, Font* font, int32_t fontIndex, const char* text);
+static bool N3DSRenderer_tryDrawSingleGlyphTextFast(
+ Renderer* base,
+ N3DSRenderer* renderer,
+ Font* font,
+ const char* text,
+ int32_t len,
+ float x,
+ float y,
+ float effectiveXScale,
+ float effectiveYScale,
+ float angleDeg,
+ bool gradient,
+ int32_t c1,
+ float alpha,
+ bool useDirectFontAsset,
+ const C2D_Image* directFontImage,
+ const Tex3DS_SubTexture* directFontBaseSubtex,
+ const N3DSAtlasFragment* fontFragment,
+ N3DSLoadedAtlasPage* fontPage
+);
+static bool N3DSRenderer_drawPackedTileEntry(Renderer* base, N3DSRenderer* renderer, const N3DSTileAtlasEntry* tileEntry, float drawX, float drawY, float xscale, float yscale, uint32_t color, float alpha);
+static bool N3DSRenderer_isFragmentedAtlasVersion(uint16_t atlasVersion);
+static bool N3DSRenderer_isPackedAtlasVersion(uint16_t atlasVersion);
+static const char* N3DSRenderer_getTextureFormatName(uint32_t textureFormat);
+static void N3DSRenderer_sceneBeginTarget(N3DSRenderer* renderer, uint8_t targetKind, bool force);
+static void N3DSRenderer_freeTileLayerChunkCache(N3DSRenderer* renderer, N3DSTileLayerChunkCache* cache);
+static void N3DSRenderer_freeDirectTextureAsset(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer);
+static void N3DSRenderer_unloadDirectTextureBlob(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer);
+static bool N3DSRenderer_evictLRUDirectAsset(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset);
+static bool N3DSRenderer_evictLRUDirectAssetBlob(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset);
+static void N3DSRenderer_buildDirectAssetMaps(N3DSRenderer* renderer);
+static bool N3DSRenderer_tryDrawDirectMappedSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha);
+static bool N3DSRenderer_tryResolveDirectFontImage(N3DSRenderer* renderer, int32_t fontIndex, C2D_Image* outImage, Tex3DS_SubTexture* outSubtex);
+static void N3DSRenderer_drawImage(Renderer* base, C2D_Image* image, float localX, float localY, float width, float height, float pivotX, float pivotY, float angleDeg, uint32_t color, float alpha);
+static void N3DSRenderer_getActiveTargetSize(const N3DSRenderer* renderer, float* outW, float* outH);
+static bool N3DSRenderer_isScreenRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h);
+static bool N3DSRenderer_isScreenRotatedRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h, float angleDeg);
+static bool N3DSRenderer_loadPackedDirectAssets(N3DSRenderer* renderer);
+static bool N3DSRenderer_loadRoomManifest(N3DSRenderer* renderer);
+static const N3DSRoomManifestEntry* N3DSRenderer_findRoomManifestEntry(const N3DSRenderer* renderer, uint32_t roomIndex);
+static void N3DSRenderer_prewarmRoomManifestBlobCache(N3DSRenderer* renderer, uint32_t roomIndex, bool* seenPages, uint32_t* remainingBlobBytes);
+static bool N3DSRenderer_tryLoadPackedDirectTextureBlob(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath);
+static N3DSDynamicCaptureTPAG* N3DSRenderer_getDynamicCaptureTPAG(N3DSRenderer* renderer, int32_t tpagIndex);
+static void N3DSRenderer_freeDynamicCaptureTPAG(N3DSRenderer* renderer, N3DSDynamicCaptureTPAG* capture);
+static int32_t N3DSRenderer_allocDynamicCaptureTPAG(N3DSRenderer* renderer);
+
+enum {
+ N3DS_TRACE_KIND_SPRITE = 1u << 0,
+ N3DS_TRACE_KIND_SPRITE_PART = 1u << 1,
+ N3DS_TRACE_KIND_FONT = 1u << 2,
+};
+
+static const char* N3DSRenderer_getTraceKindName(uint8_t traceKind) {
+ switch (traceKind) {
+ case N3DS_TRACE_KIND_SPRITE: return "sprite";
+ case N3DS_TRACE_KIND_SPRITE_PART: return "sprite_part";
+ case N3DS_TRACE_KIND_FONT: return "font";
+ default: return "unknown";
+ }
+}
+
+static void N3DSRenderer_traceTPAGUsage(N3DSRenderer* renderer, uint8_t traceKind, int32_t tpagIndex) {
+#if !N3DS_ENABLE_LOGGING
+ (void) renderer;
+ (void) traceKind;
+ (void) tpagIndex;
+ return;
+#else
+ if (renderer == NULL || tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return;
+ if (renderer->atlasTraceMask == NULL) return;
+
+ uint8_t* traceMask = &renderer->atlasTraceMask[tpagIndex];
+ if ((*traceMask & traceKind) != 0u) return;
+ *traceMask |= traceKind;
+
+ const char* kindName = N3DSRenderer_getTraceKindName(traceKind);
+ if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex];
+ fprintf(
+ stderr,
+ "N3DS TRACE: kind=%s tpag=%d page=%u format=%s\n",
+ kindName,
+ (int) tpagIndex,
+ (unsigned int) item->atlasId,
+ (item->atlasId < renderer->atlasPageCount) ? N3DSRenderer_getTextureFormatName(renderer->atlasPages[item->atlasId].textureFormat) : "unknown"
+ );
+ if (renderer->atlasTraceFile != NULL) {
+ fprintf(
+ renderer->atlasTraceFile,
+ "kind=%s tpag=%d page=%u format=%s\n",
+ kindName,
+ (int) tpagIndex,
+ (unsigned int) item->atlasId,
+ (item->atlasId < renderer->atlasPageCount) ? N3DSRenderer_getTextureFormatName(renderer->atlasPages[item->atlasId].textureFormat) : "unknown"
+ );
+ fflush(renderer->atlasTraceFile);
+ }
+ return;
+ }
+
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ char pagesBuf[256];
+ size_t cursor = 0;
+ pagesBuf[0] = '\0';
+ repeat(item->fragmentCount, i) {
+ uint32_t fragmentIndex = item->fragmentStart + (uint32_t) i;
+ if (fragmentIndex >= renderer->atlasFragmentCount) break;
+ uint16_t pageIndex = renderer->atlasFragments[fragmentIndex].atlasId;
+ bool alreadyListed = false;
+ repeat(i, prevIndex) {
+ uint32_t prevFragmentIndex = item->fragmentStart + (uint32_t) prevIndex;
+ if (prevFragmentIndex >= renderer->atlasFragmentCount) break;
+ if (renderer->atlasFragments[prevFragmentIndex].atlasId == pageIndex) {
+ alreadyListed = true;
+ break;
+ }
+ }
+ if (alreadyListed) continue;
+ int written = snprintf(
+ pagesBuf + cursor,
+ sizeof(pagesBuf) - cursor,
+ "%s%u",
+ cursor == 0 ? "" : ",",
+ (unsigned int) pageIndex
+ );
+ if (written <= 0 || (size_t) written >= sizeof(pagesBuf) - cursor) break;
+ cursor += (size_t) written;
+ }
+
+ fprintf(
+ stderr,
+ "N3DS TRACE: kind=%s tpag=%d fragments=%u pages=%s\n",
+ kindName,
+ (int) tpagIndex,
+ (unsigned int) item->fragmentCount,
+ pagesBuf[0] != '\0' ? pagesBuf : "none"
+ );
+ if (renderer->atlasTraceFile != NULL) {
+ fprintf(
+ renderer->atlasTraceFile,
+ "kind=%s tpag=%d fragments=%u pages=%s\n",
+ kindName,
+ (int) tpagIndex,
+ (unsigned int) item->fragmentCount,
+ pagesBuf[0] != '\0' ? pagesBuf : "none"
+ );
+ fflush(renderer->atlasTraceFile);
+ }
+#endif
+}
+
+static void N3DSRenderer_computeFrameLayoutForTarget(N3DSRenderer* renderer, int32_t gameW, int32_t gameH, int32_t targetW, int32_t targetH) {
+ if (gameW <= 0) gameW = targetW;
+ if (gameH <= 0) gameH = targetH;
+
+ float sx = (float) targetW / (float) gameW;
+ float sy = (float) targetH / (float) gameH;
+ float scale = sx < sy ? sx : sy;
+ renderer->frameScaleX = scale;
+ renderer->frameScaleY = scale;
+ renderer->frameOffsetX = ((float) targetW - ((float) gameW * scale)) * 0.5f;
+ renderer->frameOffsetY = ((float) targetH - ((float) gameH * scale)) * 0.5f;
+}
+
+static void N3DSRenderer_setTopBattle320x240Layout(N3DSRenderer* renderer, int32_t guiW, int32_t guiH, float yOffset) {
+ if (renderer == NULL) return;
+ if (guiW <= 0) guiW = 640;
+ if (guiH <= 0) guiH = 480;
+
+ renderer->viewX = 0;
+ renderer->viewY = 0;
+ renderer->frameScaleX = (float) N3DS_BOTTOM_WIDTH / (float) guiW;
+ renderer->frameScaleY = (float) N3DS_BOTTOM_HEIGHT / (float) guiH;
+ renderer->viewScaleX = renderer->frameScaleX;
+ renderer->viewScaleY = renderer->frameScaleY;
+ renderer->portOffsetX = 0.0f;
+ renderer->portOffsetY = 0.0f;
+ renderer->frameOffsetX = floorf((((float) N3DS_TOP_WIDTH - (float) N3DS_BOTTOM_WIDTH) * 0.5f) + 0.5f);
+ renderer->frameOffsetY = floorf(yOffset + 0.5f);
+}
+
+static bool N3DSRenderer_isFragmentedAtlasVersion(uint16_t atlasVersion) {
+ return atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED ||
+ atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILES ||
+ atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS ||
+ atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT ||
+ atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT ||
+ atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED;
+}
+
+static bool N3DSRenderer_isPackedAtlasVersion(uint16_t atlasVersion) {
+ return atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED;
+}
+
+static double N3DSRenderer_ticksToMs(u64 ticks) {
+ return (double) ticks * 1000.0 / (double) SYSCLOCK_ARM11;
+}
+
+static void N3DSRenderer_resetFramePerfCounters(N3DSRenderer* renderer) {
+ renderer->frameBlobReads = 0;
+ renderer->framePageImports = 0;
+ renderer->frameImportFailures = 0;
+ renderer->frameImportEvictions = 0;
+ renderer->frameFragmentDraws = 0;
+ renderer->frameSpriteDrawCalls = 0;
+ renderer->frameSpritePartDrawCalls = 0;
+ renderer->frameDirectSpriteHits = 0;
+ renderer->frameDirectAssetLoads = 0;
+ renderer->frameTextureSwitches = 0;
+ renderer->frameTextGlyphDraws = 0;
+ renderer->frameTextTenthsMs = 0;
+ renderer->pendingC2DDraws = 0;
+ renderer->lastDrawTexture = NULL;
+ renderer->lastDirectTPAGIndex = -1;
+ renderer->lastDirectTPAGImage = NULL;
+}
+
+static void N3DSRenderer_logPerfWindowIfNeeded(N3DSRenderer* renderer) {
+#ifndef N3DS_ENABLE_PERF_LOGS
+ (void) renderer;
+ return;
+#else
+ renderer->perfWindowFrames++;
+ renderer->perfWindowBlobReads += renderer->frameBlobReads;
+ renderer->perfWindowPageImports += renderer->framePageImports;
+ renderer->perfWindowImportFailures += renderer->frameImportFailures;
+ renderer->perfWindowImportEvictions += renderer->frameImportEvictions;
+ renderer->perfWindowFragmentDraws += renderer->frameFragmentDraws;
+ renderer->perfWindowSpriteDrawCalls += renderer->frameSpriteDrawCalls;
+ renderer->perfWindowSpritePartDrawCalls += renderer->frameSpritePartDrawCalls;
+
+ if (renderer->perfWindowFrames < N3DS_PERF_LOG_INTERVAL_FRAMES) return;
+
+ fprintf(
+ stderr,
+ "N3DS perf/%lu: blobReads=%lu pageImports=%lu importFail=%lu evictions=%lu fragDraws=%lu spriteCalls=%lu partCalls=%lu resident=%lu blobKB=%lu\n",
+ (unsigned long) renderer->perfWindowFrames,
+ (unsigned long) renderer->perfWindowBlobReads,
+ (unsigned long) renderer->perfWindowPageImports,
+ (unsigned long) renderer->perfWindowImportFailures,
+ (unsigned long) renderer->perfWindowImportEvictions,
+ (unsigned long) renderer->perfWindowFragmentDraws,
+ (unsigned long) renderer->perfWindowSpriteDrawCalls,
+ (unsigned long) renderer->perfWindowSpritePartDrawCalls,
+ (unsigned long) renderer->residentAtlasPageCount,
+ (unsigned long) (renderer->cachedT3xBytes / 1024u)
+ );
+
+ renderer->perfWindowFrames = 0;
+ renderer->perfWindowBlobReads = 0;
+ renderer->perfWindowPageImports = 0;
+ renderer->perfWindowImportFailures = 0;
+ renderer->perfWindowImportEvictions = 0;
+ renderer->perfWindowFragmentDraws = 0;
+ renderer->perfWindowSpriteDrawCalls = 0;
+ renderer->perfWindowSpritePartDrawCalls = 0;
+#endif
+}
+
+static uint16_t N3DS_readU16(const uint8_t* ptr) {
+ return (uint16_t) (ptr[0] | (ptr[1] << 8));
+}
+
+static uint32_t N3DS_readU32(const uint8_t* ptr) {
+ return (uint32_t) ptr[0] |
+ ((uint32_t) ptr[1] << 8) |
+ ((uint32_t) ptr[2] << 16) |
+ ((uint32_t) ptr[3] << 24);
+}
+
+static uint32_t N3DSRenderer_getTextureFormatBytesPerPixel(uint32_t textureFormat) {
+ switch (textureFormat) {
+ case N3DS_TEXFMT_ETC1A4: return 1u;
+ case N3DS_TEXFMT_INDEXED8: return 2u;
+ case N3DS_TEXFMT_HYBRID: return 1u;
+ case N3DS_TEXFMT_LA4: return 1u;
+ case N3DS_TEXFMT_L4: return 0u;
+ case N3DS_TEXFMT_RGBA5551:
+ default: return 2u;
+ }
+}
+
+static GPU_TEXCOLOR N3DSRenderer_getGPUTextureFormat(uint32_t textureFormat) {
+ switch (textureFormat) {
+ case N3DS_TEXFMT_ETC1A4: return GPU_ETC1A4;
+ case N3DS_TEXFMT_INDEXED8: return GPU_RGBA5551;
+ case N3DS_TEXFMT_HYBRID: return GPU_ETC1A4;
+ case N3DS_TEXFMT_L4: return GPU_L4;
+ case N3DS_TEXFMT_LA4: return GPU_LA4;
+ case N3DS_TEXFMT_RGBA5551:
+ default: return GPU_RGBA5551;
+ }
+}
+
+static const char* N3DSRenderer_getTextureFormatName(uint32_t textureFormat) {
+ switch (textureFormat) {
+ case N3DS_TEXFMT_ETC1A4: return "etc1a4";
+ case N3DS_TEXFMT_INDEXED8: return "indexed8";
+ case N3DS_TEXFMT_HYBRID: return "hybrid";
+ case N3DS_TEXFMT_L4: return "l4";
+ case N3DS_TEXFMT_LA4: return "la4";
+ case N3DS_TEXFMT_RGBA5551:
+ default: return "rgba5551";
+ }
+}
+
+static uint32_t N3DSRenderer_getPageVRAMBytes(MAYBE_UNUSED const N3DSRenderer* renderer, const N3DSLoadedAtlasPage* page) {
+ if (page == NULL) return 0;
+ uint32_t texelCount = (uint32_t) page->width * (uint32_t) page->height;
+ switch (page->textureFormat) {
+ case N3DS_TEXFMT_L4:
+ return texelCount / 2u;
+ case N3DS_TEXFMT_LA4:
+ case N3DS_TEXFMT_ETC1A4:
+ case N3DS_TEXFMT_HYBRID:
+ return texelCount;
+ case N3DS_TEXFMT_INDEXED8:
+ case N3DS_TEXFMT_RGBA5551:
+ default:
+ return texelCount * 2u;
+ }
+}
+
+static void N3DSRenderer_getActiveTargetSize(const N3DSRenderer* renderer, float* outW, float* outH) {
+ if (outW == NULL || outH == NULL) return;
+
+ bool useBottomTarget =
+ renderer != NULL &&
+ (renderer->activeSceneTarget == N3DS_SCENE_TARGET_BOTTOM || renderer->bottomScreenGuiActive);
+ *outW = useBottomTarget ? (float) N3DS_BOTTOM_WIDTH : (float) N3DS_TOP_WIDTH;
+ *outH = useBottomTarget ? (float) N3DS_BOTTOM_HEIGHT : (float) N3DS_TOP_HEIGHT;
+}
+
+static bool N3DSRenderer_isScreenRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h) {
+ if (w < 0.0f) {
+ x += w;
+ w = -w;
+ }
+ if (h < 0.0f) {
+ y += h;
+ h = -h;
+ }
+
+ float targetW = 0.0f;
+ float targetH = 0.0f;
+ N3DSRenderer_getActiveTargetSize(renderer, &targetW, &targetH);
+ return x >= targetW || y >= targetH || (x + w) <= 0.0f || (y + h) <= 0.0f;
+}
+
+static bool N3DSRenderer_isScreenRotatedRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h, float angleDeg) {
+ if (fabsf(angleDeg) < 0.001f) {
+ return N3DSRenderer_isScreenRectOffscreen(renderer, x, y, w, h);
+ }
+
+ if (w < 0.0f) {
+ x += w;
+ w = -w;
+ }
+ if (h < 0.0f) {
+ y += h;
+ h = -h;
+ }
+
+ float halfW = w * 0.5f;
+ float halfH = h * 0.5f;
+ float centerX = x + halfW;
+ float centerY = y + halfH;
+ float radius = sqrtf(halfW * halfW + halfH * halfH);
+
+ float targetW = 0.0f;
+ float targetH = 0.0f;
+ N3DSRenderer_getActiveTargetSize(renderer, &targetW, &targetH);
+ return (centerX - radius) >= targetW || (centerY - radius) >= targetH ||
+ (centerX + radius) <= 0.0f || (centerY + radius) <= 0.0f;
+}
+
+static void N3DSRenderer_freeDirectTextureAsset(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer) {
+ if (asset == NULL || !asset->ready) {
+ if (asset != NULL) {
+ asset->sheet = NULL;
+ asset->ready = false;
+ asset->pinned = false;
+ }
+ return;
+ }
+
+ if (asset->sheet != NULL) {
+ C2D_SpriteSheetFree(asset->sheet);
+ asset->sheet = NULL;
+ }
+ asset->image.tex = NULL;
+ asset->image.subtex = NULL;
+ if (renderer != NULL) {
+ if (renderer->residentDirectAssetVRAMBytes >= asset->vramBytes) renderer->residentDirectAssetVRAMBytes -= asset->vramBytes;
+ else renderer->residentDirectAssetVRAMBytes = 0;
+ }
+ asset->vramBytes = 0;
+ asset->ready = false;
+ asset->pinned = false;
+ asset->lastUsedStamp = 0;
+ asset->lastUsedFrame = 0;
+}
+
+static void N3DSRenderer_unloadDirectTextureBlob(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer) {
+ if (asset == NULL || asset->blobData == NULL) return;
+
+ free(asset->blobData);
+ asset->blobData = NULL;
+ if (renderer != NULL) {
+ if (renderer->cachedDirectT3xBytes >= asset->blobSize) renderer->cachedDirectT3xBytes -= asset->blobSize;
+ else renderer->cachedDirectT3xBytes = 0;
+ }
+ asset->blobSize = 0;
+ asset->blobLastUsedStamp = 0;
+}
+
+static void N3DSRenderer_buildSheetFrameImages(N3DSDirectSpriteAsset* spriteAsset) {
+ if (spriteAsset == NULL) return;
+ if (spriteAsset->sheetAsset.sheet == NULL || spriteAsset->sheetFrameCount == 0) return;
+ if (spriteAsset->sheetFrameImages != NULL) return;
+
+ spriteAsset->sheetFrameImages = safeCalloc(spriteAsset->sheetFrameCount, sizeof(C2D_Image));
+ repeat(spriteAsset->sheetFrameCount, i) {
+ spriteAsset->sheetFrameImages[i] = C2D_SpriteSheetGetImage(spriteAsset->sheetAsset.sheet, i);
+ }
+}
+
+static void N3DSRenderer_clearDirectAssetPins(N3DSRenderer* renderer) {
+ if (renderer == NULL) return;
+
+ repeat(renderer->directSpriteAssetCount, spriteIndex) {
+ N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex];
+ spriteAsset->sheetAsset.pinned = false;
+ repeat(spriteAsset->frameCount, frameIndex) {
+ spriteAsset->frameAssets[frameIndex].pinned = false;
+ }
+ }
+
+ repeat(renderer->directBackgroundAssetCount, bgIndex) {
+ renderer->directBackgroundAssets[bgIndex].pinned = false;
+ }
+
+ repeat(renderer->directFontAssetCount, fontIndex) {
+ renderer->directFontAssets[fontIndex].pinned = false;
+ }
+}
+
+static void N3DSRenderer_flushRoomDirectAssets(N3DSRenderer* renderer) {
+ if (renderer == NULL) return;
+
+ repeat(renderer->directSpriteAssetCount, spriteIndex) {
+ N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex];
+ if (!spriteAsset->sheetAsset.pinned) {
+ N3DSRenderer_freeDirectTextureAsset(&spriteAsset->sheetAsset, renderer);
+ N3DSRenderer_unloadDirectTextureBlob(&spriteAsset->sheetAsset, renderer);
+ }
+
+ repeat(spriteAsset->frameCount, frameIndex) {
+ N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex];
+ if (frameAsset->pinned) continue;
+ N3DSRenderer_freeDirectTextureAsset(frameAsset, renderer);
+ N3DSRenderer_unloadDirectTextureBlob(frameAsset, renderer);
+ }
+ }
+
+ repeat(renderer->directBackgroundAssetCount, bgIndex) {
+ N3DSRenderer_freeDirectTextureAsset(&renderer->directBackgroundAssets[bgIndex], renderer);
+ N3DSRenderer_unloadDirectTextureBlob(&renderer->directBackgroundAssets[bgIndex], renderer);
+ }
+}
+
+static void N3DSRenderer_buildDirectAssetMaps(N3DSRenderer* renderer) {
+#if !N3DS_ENABLE_DIRECT_ASSETS
+ if (renderer == NULL) return;
+ renderer->directSpriteAssetCount = 0;
+ renderer->directBackgroundAssetCount = 0;
+ renderer->directFontAssetCount = 0;
+ renderer->directSpriteAssets = NULL;
+ renderer->directBackgroundAssets = NULL;
+ renderer->directFontAssets = NULL;
+ renderer->tpagToSpriteIndex = NULL;
+ renderer->tpagToSpriteFrameIndex = NULL;
+ renderer->tpagToBackgroundIndex = NULL;
+ return;
+#endif
+ if (renderer == NULL || renderer->base.dataWin == NULL) return;
+
+ DataWin* dw = renderer->base.dataWin;
+ uint32_t tpagCount = dw->tpag.count > 0 ? dw->tpag.count : 1u;
+ renderer->tpagToSpriteIndex = safeMalloc((size_t) tpagCount * sizeof(int32_t));
+ renderer->tpagToSpriteFrameIndex = safeMalloc((size_t) tpagCount * sizeof(int32_t));
+ renderer->tpagToBackgroundIndex = safeMalloc((size_t) tpagCount * sizeof(int32_t));
+ repeat(tpagCount, i) {
+ renderer->tpagToSpriteIndex[i] = -1;
+ renderer->tpagToSpriteFrameIndex[i] = -1;
+ renderer->tpagToBackgroundIndex[i] = -1;
+ }
+
+ renderer->directSpriteAssetCount = dw->sprt.count;
+ renderer->directBackgroundAssetCount = dw->bgnd.count;
+ renderer->directFontAssetCount = dw->font.count;
+ renderer->directSpriteAssets = safeCalloc(renderer->directSpriteAssetCount > 0 ? renderer->directSpriteAssetCount : 1u, sizeof(N3DSDirectSpriteAsset));
+ renderer->directBackgroundAssets = safeCalloc(renderer->directBackgroundAssetCount > 0 ? renderer->directBackgroundAssetCount : 1u, sizeof(N3DSDirectTextureAsset));
+ renderer->directFontAssets = safeCalloc(renderer->directFontAssetCount > 0 ? renderer->directFontAssetCount : 1u, sizeof(N3DSDirectTextureAsset));
+
+ repeat(dw->sprt.count, spriteIndex) {
+ Sprite* sprite = &dw->sprt.sprites[spriteIndex];
+ N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex];
+ spriteAsset->frameCount = sprite->textureCount;
+ if (sprite->textureCount > 0) {
+ spriteAsset->frameAssets = safeCalloc((size_t) sprite->textureCount, sizeof(N3DSDirectTextureAsset));
+ }
+
+ repeat(sprite->textureCount, frameIndex) {
+ int32_t tpagIndex = sprite->tpagIndices[frameIndex];
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) continue;
+ if (renderer->tpagToSpriteIndex[tpagIndex] < 0) {
+ renderer->tpagToSpriteIndex[tpagIndex] = (int32_t) spriteIndex;
+ renderer->tpagToSpriteFrameIndex[tpagIndex] = (int32_t) frameIndex;
+ }
+ }
+ }
+
+ repeat(dw->bgnd.count, bgIndex) {
+ int32_t tpagIndex = dw->bgnd.backgrounds[bgIndex].tpagIndex;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) continue;
+ if (renderer->tpagToBackgroundIndex[tpagIndex] < 0) {
+ renderer->tpagToBackgroundIndex[tpagIndex] = (int32_t) bgIndex;
+ }
+ }
+}
+
+static bool N3DS_pathExists(const char* path) {
+ struct stat st;
+ return path != NULL && stat(path, &st) == 0;
+}
+
+static void N3DSRenderer_configureFileBuffer(FILE* file) {
+ if (file == NULL) return;
+ setvbuf(file, NULL, _IOFBF, N3DS_RENDERER_FILE_BUFFER_SIZE);
+}
+
+static char* N3DSRenderer_getCachedAssetPath(N3DSRenderer* renderer, const char* relativePath) {
+ if (renderer == NULL || relativePath == NULL) return NULL;
+ ptrdiff_t idx = shgeti(renderer->resolvedAssetPathCache, relativePath);
+ if (idx < 0) return NULL;
+ return safeStrdup(renderer->resolvedAssetPathCache[idx].value);
+}
+
+static void N3DSRenderer_setCachedAssetPath(N3DSRenderer* renderer, const char* relativePath, const char* resolvedPath) {
+ if (renderer == NULL || relativePath == NULL || resolvedPath == NULL) return;
+ ptrdiff_t idx = shgeti(renderer->resolvedAssetPathCache, relativePath);
+ if (idx >= 0) {
+ free(renderer->resolvedAssetPathCache[idx].value);
+ renderer->resolvedAssetPathCache[idx].value = safeStrdup(resolvedPath);
+ return;
+ }
+
+ shput(renderer->resolvedAssetPathCache, relativePath, safeStrdup(resolvedPath));
+}
+
+static void N3DSRenderer_setStartupError(N3DSRenderer* renderer, const char* message) {
+ if (renderer == NULL) return;
+ snprintf(
+ renderer->startupError,
+ sizeof(renderer->startupError),
+ "%s",
+ message != NULL ? message : "Unknown 3DS renderer startup error"
+ );
+}
+
+static bool N3DSRenderer_resolveAssetPath(N3DSRenderer* renderer, const char* relativePath, char* resolvedPath, size_t resolvedPathSize) {
+ if (relativePath == NULL || resolvedPath == NULL || resolvedPathSize == 0) return false;
+
+ char* cachedPath = N3DSRenderer_getCachedAssetPath(renderer, relativePath);
+ if (cachedPath != NULL) {
+ snprintf(resolvedPath, resolvedPathSize, "%s", cachedPath);
+ free(cachedPath);
+ return true;
+ }
+
+ snprintf(resolvedPath, resolvedPathSize, "%s/%s", N3DS_ROMFS_ASSET_BASE, relativePath);
+ if (N3DS_pathExists(resolvedPath)) {
+ N3DSRenderer_setCachedAssetPath(renderer, relativePath, resolvedPath);
+ return true;
+ }
+
+ snprintf(resolvedPath, resolvedPathSize, "%s/%s", N3DS_SDMC_ASSET_BASE, relativePath);
+ if (N3DS_pathExists(resolvedPath)) {
+ N3DSRenderer_setCachedAssetPath(renderer, relativePath, resolvedPath);
+ return true;
+ }
+
+ resolvedPath[0] = '\0';
+ return false;
+}
+
+static FILE* N3DSRenderer_openAssetFile(N3DSRenderer* renderer, const char* relativePath, char* resolvedPath, size_t resolvedPathSize) {
+ if (!N3DSRenderer_resolveAssetPath(renderer, relativePath, resolvedPath, resolvedPathSize)) return NULL;
+ FILE* file = fopen(resolvedPath, "rb");
+ N3DSRenderer_configureFileBuffer(file);
+ return file;
+}
+
+static bool N3DSRenderer_loadPackedDirectAssets(N3DSRenderer* renderer) {
+ if (renderer == NULL) return false;
+ if (renderer->packedDirectAssetFile != NULL) return true;
+
+ char resolvedPath[512];
+ FILE* file = N3DSRenderer_openAssetFile(renderer, "direct_assets.bin", resolvedPath, sizeof(resolvedPath));
+ if (file == NULL) return false;
+
+ uint8_t header[16];
+ if (fread(header, 1, sizeof(header), file) != sizeof(header)) {
+ fclose(file);
+ return false;
+ }
+
+ uint32_t magic = N3DS_readU32(header + 0);
+ uint32_t version = N3DS_readU32(header + 4);
+ uint32_t entryCount = N3DS_readU32(header + 8);
+ uint32_t stringTableSize = N3DS_readU32(header + 12);
+ if (magic != N3DS_DIRECT_ASSET_MAGIC || version != N3DS_DIRECT_ASSET_VERSION) {
+ fclose(file);
+ return false;
+ }
+ if (entryCount > (UINT32_MAX / N3DS_DIRECT_ASSET_ENTRY_SIZE)) {
+ fclose(file);
+ return false;
+ }
+
+ uint32_t entryTableSize = entryCount * N3DS_DIRECT_ASSET_ENTRY_SIZE;
+ uint64_t metadataSize64 = 16u + (uint64_t) entryTableSize + (uint64_t) stringTableSize;
+ if (fseek(file, 0, SEEK_END) != 0) {
+ fclose(file);
+ return false;
+ }
+ long fileSizeLong = ftell(file);
+ if (fileSizeLong <= 0 || metadataSize64 > (uint64_t) fileSizeLong) {
+ fclose(file);
+ return false;
+ }
+ uint32_t fileSize = (uint32_t) fileSizeLong;
+ if (fseek(file, 16, SEEK_SET) != 0) {
+ fclose(file);
+ return false;
+ }
+
+ uint8_t* entryTable = safeMalloc(entryTableSize > 0 ? (size_t) entryTableSize : 1u);
+ uint8_t* stringTable = safeMalloc(stringTableSize > 0 ? (size_t) stringTableSize : 1u);
+ bool ok = true;
+ if (entryTableSize > 0) {
+ ok = fread(entryTable, 1, (size_t) entryTableSize, file) == (size_t) entryTableSize;
+ }
+ if (ok && stringTableSize > 0) {
+ ok = fread(stringTable, 1, (size_t) stringTableSize, file) == (size_t) stringTableSize;
+ }
+ if (!ok) {
+ free(entryTable);
+ free(stringTable);
+ fclose(file);
+ return false;
+ }
+
+ if (renderer->packedDirectAssetMap != NULL) {
+ shfree(renderer->packedDirectAssetMap);
+ renderer->packedDirectAssetMap = NULL;
+ }
+ sh_new_strdup(renderer->packedDirectAssetMap);
+
+ repeat(entryCount, i) {
+ const uint8_t* row = entryTable + ((size_t) i * N3DS_DIRECT_ASSET_ENTRY_SIZE);
+ uint32_t pathOffset = N3DS_readU32(row + 0);
+ uint32_t dataOffset = N3DS_readU32(row + 4);
+ uint32_t dataSize = N3DS_readU32(row + 8);
+ if (pathOffset >= stringTableSize || dataOffset > fileSize || dataSize > fileSize || dataOffset + dataSize > fileSize) {
+ ok = false;
+ break;
+ }
+
+ const char* path = (const char*) (stringTable + pathOffset);
+ size_t remaining = (size_t) (stringTableSize - pathOffset);
+ if (memchr(path, '\0', remaining) == NULL) {
+ ok = false;
+ break;
+ }
+
+ N3DSPackedDirectAssetEntry entry = {
+ .dataOffset = dataOffset,
+ .dataSize = dataSize,
+ };
+ shput(renderer->packedDirectAssetMap, path, entry);
+ }
+
+ free(entryTable);
+ free(stringTable);
+ if (!ok) {
+ shfree(renderer->packedDirectAssetMap);
+ renderer->packedDirectAssetMap = NULL;
+ fclose(file);
+ return false;
+ }
+
+ renderer->packedDirectAssetFile = file;
+ fprintf(stderr, "N3DS: loaded packed direct texture blob (%u entries)\n", (unsigned int) entryCount);
+ return true;
+}
+
+static bool N3DSRenderer_loadRoomManifest(N3DSRenderer* renderer) {
+ if (renderer == NULL) return false;
+ if (renderer->roomManifestEntries != NULL) return true;
+
+ char resolvedPath[512];
+ FILE* file = N3DSRenderer_openAssetFile(renderer, "room_manifest.bin", resolvedPath, sizeof(resolvedPath));
+ if (file == NULL) return false;
+
+ uint8_t header[20];
+ if (fread(header, 1, sizeof(header), file) != sizeof(header)) {
+ fclose(file);
+ return false;
+ }
+
+ uint32_t magic = N3DS_readU32(header + 0);
+ uint32_t version = N3DS_readU32(header + 4);
+ uint32_t entryCount = N3DS_readU32(header + 8);
+ uint32_t pageRefCount = N3DS_readU32(header + 12);
+ uint32_t roomCount = N3DS_readU32(header + 16);
+ if (magic != N3DS_ROOM_MANIFEST_MAGIC || version != N3DS_ROOM_MANIFEST_VERSION) {
+ fclose(file);
+ return false;
+ }
+
+ uint64_t requiredBytes =
+ sizeof(header) +
+ (uint64_t) entryCount * sizeof(N3DSRoomManifestEntry) +
+ (uint64_t) pageRefCount * sizeof(N3DSRoomManifestPageRef);
+ if (fseek(file, 0, SEEK_END) != 0) {
+ fclose(file);
+ return false;
+ }
+ long fileSize = ftell(file);
+ if (fileSize < 0 || requiredBytes > (uint64_t) fileSize) {
+ fclose(file);
+ return false;
+ }
+ if (fseek(file, (long) sizeof(header), SEEK_SET) != 0) {
+ fclose(file);
+ return false;
+ }
+
+ renderer->roomManifestEntries = safeCalloc(entryCount > 0 ? entryCount : 1u, sizeof(N3DSRoomManifestEntry));
+ renderer->roomManifestPageRefs = safeCalloc(pageRefCount > 0 ? pageRefCount : 1u, sizeof(N3DSRoomManifestPageRef));
+ renderer->roomManifestEntryCount = entryCount;
+ renderer->roomManifestPageRefCount = pageRefCount;
+ renderer->roomManifestRoomCount = roomCount;
+
+ bool ok = true;
+ if (entryCount > 0) {
+ ok = fread(renderer->roomManifestEntries, sizeof(N3DSRoomManifestEntry), entryCount, file) == entryCount;
+ }
+ if (ok && pageRefCount > 0) {
+ ok = fread(renderer->roomManifestPageRefs, sizeof(N3DSRoomManifestPageRef), pageRefCount, file) == pageRefCount;
+ }
+ fclose(file);
+
+ if (!ok) {
+ free(renderer->roomManifestEntries);
+ free(renderer->roomManifestPageRefs);
+ renderer->roomManifestEntries = NULL;
+ renderer->roomManifestPageRefs = NULL;
+ renderer->roomManifestEntryCount = 0;
+ renderer->roomManifestPageRefCount = 0;
+ renderer->roomManifestRoomCount = 0;
+ return false;
+ }
+
+ fprintf(
+ stderr,
+ "N3DS: loaded room manifest (%u rooms, %u entries, %u page refs)\n",
+ roomCount,
+ entryCount,
+ pageRefCount
+ );
+ return true;
+}
+
+static const N3DSRoomManifestEntry* N3DSRenderer_findRoomManifestEntry(const N3DSRenderer* renderer, uint32_t roomIndex) {
+ if (renderer == NULL || renderer->roomManifestEntries == NULL) return NULL;
+
+ repeat(renderer->roomManifestEntryCount, i) {
+ const N3DSRoomManifestEntry* entry = &renderer->roomManifestEntries[i];
+ if (entry->roomIndex == roomIndex) return entry;
+ }
+ return NULL;
+}
+
+static void N3DSRenderer_prewarmRoomManifestBlobCache(N3DSRenderer* renderer, uint32_t roomIndex, bool* seenPages, uint32_t* remainingBlobBytes) {
+ if (renderer == NULL || seenPages == NULL || remainingBlobBytes == NULL || *remainingBlobBytes == 0) return;
+
+ const N3DSRoomManifestEntry* entry = N3DSRenderer_findRoomManifestEntry(renderer, roomIndex);
+ if (entry == NULL || entry->pageCount == 0) return;
+ if (entry->pageStart > renderer->roomManifestPageRefCount) return;
+ if (entry->pageCount > renderer->roomManifestPageRefCount - entry->pageStart) return;
+
+ repeat(entry->pageCount, i) {
+ const N3DSRoomManifestPageRef* pageRef = &renderer->roomManifestPageRefs[entry->pageStart + (uint32_t) i];
+ (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, pageRef->pageIndex, remainingBlobBytes);
+ if (*remainingBlobBytes == 0) break;
+ }
+}
+
+static bool N3DSRenderer_tryLoadPackedDirectTextureBlob(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath) {
+ if (renderer == NULL || asset == NULL || relativePath == NULL) return false;
+ if (renderer->packedDirectAssetFile == NULL || renderer->packedDirectAssetMap == NULL) return false;
+
+ ptrdiff_t mapIndex = shgeti(renderer->packedDirectAssetMap, relativePath);
+ if (mapIndex < 0) return false;
+
+ N3DSPackedDirectAssetEntry entry = renderer->packedDirectAssetMap[mapIndex].value;
+ if (entry.dataSize == 0) return false;
+
+ while (renderer->cachedDirectT3xBytes + entry.dataSize > renderer->cachedDirectT3xByteLimit) {
+ if (!N3DSRenderer_evictLRUDirectAssetBlob(renderer, asset)) break;
+ }
+
+ uint8_t* blobData = safeMalloc((size_t) entry.dataSize);
+ if (blobData == NULL) return false;
+
+ if (fseek(renderer->packedDirectAssetFile, (long) entry.dataOffset, SEEK_SET) != 0) {
+ free(blobData);
+ return false;
+ }
+ if (fread(blobData, 1, (size_t) entry.dataSize, renderer->packedDirectAssetFile) != (size_t) entry.dataSize) {
+ free(blobData);
+ return false;
+ }
+
+ asset->blobData = blobData;
+ asset->blobSize = entry.dataSize;
+ asset->blobLastUsedStamp = ++renderer->directBlobUseCounter;
+ renderer->cachedDirectT3xBytes += entry.dataSize;
+ renderer->frameDirectBlobReads++;
+ return true;
+}
+
+static void N3DSRenderer_unloadPage(N3DSRenderer* renderer, uint32_t pageIndex) {
+ if (pageIndex >= renderer->atlasPageCount) return;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex];
+ if (!page->ready) return;
+ C2D_Flush();
+ renderer->pendingC2DDraws = 0;
+ renderer->lastDrawTexture = NULL;
+
+ uint32_t pageBytes = N3DSRenderer_getPageVRAMBytes(renderer, page);
+
+ if (page->t3x != NULL) {
+ Tex3DS_TextureFree(page->t3x);
+ page->t3x = NULL;
+ }
+ C3D_TexDelete(&page->texture);
+ memset(&page->texture, 0, sizeof(page->texture));
+ page->ready = false;
+ page->lastUsedStamp = 0;
+ if (renderer->residentAtlasVRAMBytes >= pageBytes) renderer->residentAtlasVRAMBytes -= pageBytes;
+ else renderer->residentAtlasVRAMBytes = 0;
+ if (renderer->residentAtlasPageCount > 0) {
+ renderer->residentAtlasPageCount--;
+ }
+}
+
+static void N3DSRenderer_unloadPageBlob(N3DSRenderer* renderer, uint32_t pageIndex) {
+ if (pageIndex >= renderer->atlasPageCount) return;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex];
+ if (page->t3xData == NULL) return;
+
+ free(page->t3xData);
+ page->t3xData = NULL;
+ if (renderer->cachedT3xBytes >= page->t3xSize) renderer->cachedT3xBytes -= page->t3xSize;
+ else renderer->cachedT3xBytes = 0;
+ page->t3xSize = 0;
+ page->blobLastUsedStamp = 0;
+}
+
+static void N3DSRenderer_clearAtlasPagePins(N3DSRenderer* renderer) {
+ if (renderer == NULL) return;
+ repeat(renderer->atlasPageCount, i) {
+ renderer->atlasPages[i].pinned = false;
+ }
+}
+
+static void N3DSRenderer_flushRoomAtlasResidencyOld3DS(N3DSRenderer* renderer) {
+ if (renderer == NULL || renderer->isNew3DS) return;
+
+ repeat(renderer->atlasPageCount, i) {
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[i];
+ if (!page->ready) continue;
+ if (page->pinned) continue;
+ N3DSRenderer_unloadPage(renderer, (uint32_t) i);
+ }
+}
+
+static bool N3DSRenderer_evictLRUPageBlob(N3DSRenderer* renderer, uint32_t excludePageIndex) {
+ uint32_t bestIndex = UINT32_MAX;
+ uint32_t bestStamp = UINT32_MAX;
+
+ repeat(renderer->atlasPageCount, i) {
+ if ((uint32_t) i == excludePageIndex) continue;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[i];
+ if (page->t3xData == NULL || page->t3xSize == 0) continue;
+ if (page->blobLastUsedStamp < bestStamp) {
+ bestStamp = page->blobLastUsedStamp;
+ bestIndex = (uint32_t) i;
+ }
+ }
+
+ if (bestIndex == UINT32_MAX) return false;
+ N3DSRenderer_unloadPageBlob(renderer, bestIndex);
+ return true;
+}
+
+static bool N3DSRenderer_ensurePageBlobLoaded(N3DSRenderer* renderer, uint32_t pageIndex) {
+ if (pageIndex >= renderer->atlasPageCount) return false;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex];
+ if (page->t3xData != NULL && page->t3xSize > 0) {
+ page->blobLastUsedStamp = ++renderer->blobUseCounter;
+ return true;
+ }
+
+ FILE* pageFile = NULL;
+ uint32_t size = 0;
+ uint32_t dataOffset = 0;
+ bool usingPackedAtlas = false;
+ char assetPath[256];
+ char pageName[32];
+
+ if (N3DSRenderer_isPackedAtlasVersion(renderer->atlasVersion) &&
+ renderer->packedAtlasFile != NULL &&
+ page->dataSize > 0) {
+ pageFile = renderer->packedAtlasFile;
+ size = page->dataSize;
+ dataOffset = page->dataOffset;
+ usingPackedAtlas = true;
+ } else {
+ const char* extension = page->textureFormat == N3DS_TEXFMT_INDEXED8 ? "i8" : "t3x";
+ snprintf(pageName, sizeof(pageName), "page_%03lu.%s", (unsigned long) pageIndex, extension);
+ pageFile = N3DSRenderer_openAssetFile(renderer, pageName, assetPath, sizeof(assetPath));
+ if (pageFile == NULL) {
+ fprintf(stderr, "N3DS: missing %s\n", pageName);
+ return false;
+ }
+
+ fseek(pageFile, 0, SEEK_END);
+ long t3xSize = ftell(pageFile);
+ fseek(pageFile, 0, SEEK_SET);
+ if (t3xSize <= 0) {
+ fclose(pageFile);
+ fprintf(stderr, "N3DS: empty %s\n", assetPath);
+ return false;
+ }
+
+ size = (uint32_t) t3xSize;
+ }
+ while (renderer->cachedT3xBytes + size > renderer->cachedT3xByteLimit) {
+ if (!N3DSRenderer_evictLRUPageBlob(renderer, pageIndex)) break;
+ }
+
+ page->t3xData = safeMalloc((size_t) size);
+ page->t3xSize = size;
+ if (usingPackedAtlas) {
+ fseek(pageFile, (long) dataOffset, SEEK_SET);
+ }
+ if (fread(page->t3xData, 1, (size_t) size, pageFile) != (size_t) size) {
+ free(page->t3xData);
+ page->t3xData = NULL;
+ page->t3xSize = 0;
+ if (!usingPackedAtlas) fclose(pageFile);
+ fprintf(stderr, "N3DS: failed to read atlas page blob %lu\n", (unsigned long) pageIndex);
+ return false;
+ }
+ if (!usingPackedAtlas) fclose(pageFile);
+
+ renderer->cachedT3xBytes += size;
+ page->blobLastUsedStamp = ++renderer->blobUseCounter;
+ renderer->frameBlobReads++;
+ return true;
+}
+
+static bool N3DSRenderer_loadIndexed8Page(N3DSRenderer* renderer, N3DSLoadedAtlasPage* page) {
+ if (renderer == NULL || page == NULL || page->t3xData == NULL || page->t3xSize == 0) return false;
+
+ size_t pixelCount = (size_t) page->width * (size_t) page->height;
+ size_t requiredSize = (256u * sizeof(uint16_t)) + pixelCount;
+ if ((size_t) page->t3xSize < requiredSize) {
+ fprintf(stderr, "N3DS: indexed8 page blob too small (%lu < %lu)\n",
+ (unsigned long) page->t3xSize,
+ (unsigned long) requiredSize);
+ return false;
+ }
+
+ if (!C3D_TexInit(&page->texture, page->width, page->height, GPU_RGBA5551)) {
+ return false;
+ }
+ C3D_TexSetFilter(&page->texture, GPU_NEAREST, GPU_NEAREST);
+ C3D_TexSetWrap(&page->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE);
+
+ uint16_t* expanded = (uint16_t*) linearAlloc(pixelCount * sizeof(uint16_t));
+ if (expanded == NULL) {
+ C3D_TexDelete(&page->texture);
+ memset(&page->texture, 0, sizeof(page->texture));
+ return false;
+ }
+
+ const uint8_t* paletteBlob = page->t3xData;
+ const uint8_t* indexBlob = page->t3xData + (256u * sizeof(uint16_t));
+ uint16_t palette[256];
+ repeat(256u, i) {
+ palette[i] = (uint16_t) (paletteBlob[i * 2u + 0u] | ((uint16_t) paletteBlob[i * 2u + 1u] << 8));
+ }
+ repeat(pixelCount, i) {
+ expanded[i] = palette[indexBlob[i]];
+ }
+
+ C3D_TexUpload(&page->texture, expanded);
+ C3D_TexFlush(&page->texture);
+ linearFree(expanded);
+ page->ready = true;
+ renderer->framePageImports++;
+ return true;
+}
+
+static bool N3DSRenderer_loadPage(N3DSRenderer* renderer, uint32_t pageIndex) {
+ if (pageIndex >= renderer->atlasPageCount) return false;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex];
+ if (page->ready) return true;
+
+ if (renderer->atlasVersion == N3DS_ATLAS_VERSION_RAW) {
+ char assetPath[256];
+ FILE* textureFile = N3DSRenderer_openAssetFile(renderer, "textures.bin", assetPath, sizeof(assetPath));
+ if (textureFile == NULL) {
+ fprintf(stderr, "N3DS: missing textures.bin for raw atlas version\n");
+ return false;
+ }
+
+ if (!C3D_TexInit(&page->texture, page->width, page->height, N3DSRenderer_getGPUTextureFormat(page->textureFormat))) {
+ fclose(textureFile);
+ return false;
+ }
+ C3D_TexSetFilter(&page->texture, GPU_NEAREST, GPU_NEAREST);
+ C3D_TexSetWrap(&page->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE);
+
+ uint8_t* textureData = linearAlloc(page->dataSize);
+ if (textureData == NULL) {
+ fclose(textureFile);
+ C3D_TexDelete(&page->texture);
+ memset(&page->texture, 0, sizeof(page->texture));
+ return false;
+ }
+
+ fseek(textureFile, (long) page->dataOffset, SEEK_SET);
+ fread(textureData, 1, page->dataSize, textureFile);
+ fclose(textureFile);
+
+ C3D_TexUpload(&page->texture, textureData);
+ C3D_TexFlush(&page->texture);
+ linearFree(textureData);
+ page->ready = true;
+ return true;
+ }
+
+ if (!N3DSRenderer_ensurePageBlobLoaded(renderer, pageIndex)) return false;
+
+ if (page->textureFormat == N3DS_TEXFMT_INDEXED8) {
+ return N3DSRenderer_loadIndexed8Page(renderer, page);
+ }
+
+ page->t3x = Tex3DS_TextureImport(page->t3xData, page->t3xSize, &page->texture, NULL, false);
+ if (page->t3x == NULL) {
+ return false;
+ }
+
+ C3D_TexSetFilter(&page->texture, GPU_NEAREST, GPU_NEAREST);
+ C3D_TexSetWrap(&page->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE);
+ page->ready = true;
+ renderer->framePageImports++;
+ return true;
+}
+
+static bool N3DSRenderer_evictLRUPage(N3DSRenderer* renderer, uint32_t excludePageIndex) {
+ uint32_t bestIndex = UINT32_MAX;
+ uint32_t bestStamp = UINT32_MAX;
+
+ repeat(renderer->atlasPageCount, i) {
+ if ((uint32_t) i == excludePageIndex) continue;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[i];
+ if (!page->ready) continue;
+ if (page->pinned) continue;
+ if (page->lastUsedFrame == renderer->frameSequence) continue;
+ if (page->lastUsedStamp < bestStamp) {
+ bestStamp = page->lastUsedStamp;
+ bestIndex = (uint32_t) i;
+ }
+ }
+
+ if (bestIndex == UINT32_MAX) return false;
+ N3DSRenderer_unloadPage(renderer, bestIndex);
+ return true;
+}
+
+static bool N3DSRenderer_ensurePageLoaded(N3DSRenderer* renderer, uint32_t pageIndex) {
+ if (pageIndex >= renderer->atlasPageCount) return false;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex];
+ uint32_t pageBytes = N3DSRenderer_getPageVRAMBytes(renderer, page);
+ if (page->ready) {
+ page->lastUsedStamp = ++renderer->atlasUseCounter;
+ page->lastUsedFrame = renderer->frameSequence;
+ return true;
+ }
+
+ while (renderer->residentAtlasPageCount >= renderer->residentAtlasPageLimit ||
+ renderer->residentAtlasVRAMBytes + pageBytes > renderer->residentAtlasVRAMLimitBytes) {
+ if (!N3DSRenderer_evictLRUPage(renderer, pageIndex)) break;
+ }
+
+ uint32_t evictionsForLoad = 0;
+ while (!N3DSRenderer_loadPage(renderer, pageIndex)) {
+ if (!N3DSRenderer_evictLRUPage(renderer, pageIndex)) {
+ renderer->frameImportFailures++;
+ renderer->frameImportEvictions += evictionsForLoad;
+ fprintf(
+ stderr,
+ "N3DS: failed to import page %lu from RAM cache after evicting %lu pages\n",
+ (unsigned long) pageIndex,
+ (unsigned long) evictionsForLoad
+ );
+ return false;
+ }
+ evictionsForLoad++;
+ }
+
+ page->lastUsedStamp = ++renderer->atlasUseCounter;
+ page->lastUsedFrame = renderer->frameSequence;
+ renderer->residentAtlasPageCount++;
+ renderer->residentAtlasVRAMBytes += pageBytes;
+ renderer->frameImportEvictions += evictionsForLoad;
+ return true;
+}
+
+static bool N3DSRenderer_evictLRUDirectAsset(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset) {
+ if (renderer == NULL) return false;
+
+ N3DSDirectTextureAsset* bestAsset = NULL;
+ uint32_t bestStamp = UINT32_MAX;
+
+ repeat(renderer->directSpriteAssetCount, spriteIndex) {
+ N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex];
+ N3DSDirectTextureAsset* asset = &spriteAsset->sheetAsset;
+ if (asset->ready && asset != excludeAsset && asset->lastUsedFrame != renderer->frameSequence && asset->lastUsedStamp < bestStamp) {
+ if (asset->pinned) continue;
+ bestStamp = asset->lastUsedStamp;
+ bestAsset = asset;
+ }
+
+ repeat(spriteAsset->frameCount, frameIndex) {
+ asset = &spriteAsset->frameAssets[frameIndex];
+ if (!asset->ready || asset == excludeAsset) continue;
+ if (asset->lastUsedFrame == renderer->frameSequence) continue;
+ if (asset->pinned) continue;
+ if (asset->lastUsedStamp < bestStamp) {
+ bestStamp = asset->lastUsedStamp;
+ bestAsset = asset;
+ }
+ }
+ }
+
+ repeat(renderer->directBackgroundAssetCount, bgIndex) {
+ N3DSDirectTextureAsset* asset = &renderer->directBackgroundAssets[bgIndex];
+ if (!asset->ready || asset == excludeAsset) continue;
+ if (asset->lastUsedFrame == renderer->frameSequence) continue;
+ if (asset->pinned) continue;
+ if (asset->lastUsedStamp < bestStamp) {
+ bestStamp = asset->lastUsedStamp;
+ bestAsset = asset;
+ }
+ }
+
+ repeat(renderer->directFontAssetCount, fontIndex) {
+ N3DSDirectTextureAsset* asset = &renderer->directFontAssets[fontIndex];
+ if (!asset->ready || asset == excludeAsset) continue;
+ if (asset->lastUsedFrame == renderer->frameSequence) continue;
+ if (asset->pinned) continue;
+ if (asset->lastUsedStamp < bestStamp) {
+ bestStamp = asset->lastUsedStamp;
+ bestAsset = asset;
+ }
+ }
+
+ if (bestAsset == NULL) return false;
+ N3DSRenderer_freeDirectTextureAsset(bestAsset, renderer);
+ return true;
+}
+
+static bool N3DSRenderer_evictLRUDirectAssetBlob(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset) {
+ if (renderer == NULL) return false;
+
+ N3DSDirectTextureAsset* bestAsset = NULL;
+ uint32_t bestStamp = UINT32_MAX;
+
+ repeat(renderer->directSpriteAssetCount, spriteIndex) {
+ N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex];
+ N3DSDirectTextureAsset* asset = &spriteAsset->sheetAsset;
+ if (asset->blobData != NULL && !asset->ready && asset != excludeAsset && asset->blobLastUsedStamp < bestStamp) {
+ bestStamp = asset->blobLastUsedStamp;
+ bestAsset = asset;
+ }
+
+ repeat(spriteAsset->frameCount, frameIndex) {
+ asset = &spriteAsset->frameAssets[frameIndex];
+ if (asset->blobData == NULL || asset->ready || asset == excludeAsset) continue;
+ if (asset->blobLastUsedStamp < bestStamp) {
+ bestStamp = asset->blobLastUsedStamp;
+ bestAsset = asset;
+ }
+ }
+ }
+
+ repeat(renderer->directBackgroundAssetCount, bgIndex) {
+ N3DSDirectTextureAsset* asset = &renderer->directBackgroundAssets[bgIndex];
+ if (asset->blobData == NULL || asset->ready || asset == excludeAsset) continue;
+ if (asset->blobLastUsedStamp < bestStamp) {
+ bestStamp = asset->blobLastUsedStamp;
+ bestAsset = asset;
+ }
+ }
+
+ repeat(renderer->directFontAssetCount, fontIndex) {
+ N3DSDirectTextureAsset* asset = &renderer->directFontAssets[fontIndex];
+ if (asset->blobData == NULL || asset->ready || asset == excludeAsset) continue;
+ if (asset->blobLastUsedStamp < bestStamp) {
+ bestStamp = asset->blobLastUsedStamp;
+ bestAsset = asset;
+ }
+ }
+
+ if (bestAsset == NULL) return false;
+ N3DSRenderer_unloadDirectTextureBlob(bestAsset, renderer);
+ return true;
+}
+
+static bool N3DSRenderer_ensureDirectTextureBlobLoaded(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath) {
+ if (renderer == NULL || asset == NULL || relativePath == NULL) return false;
+ if (asset->blobData != NULL && asset->blobSize > 0) {
+ asset->blobLastUsedStamp = ++renderer->directBlobUseCounter;
+ return true;
+ }
+
+ if (N3DSRenderer_tryLoadPackedDirectTextureBlob(renderer, asset, relativePath)) {
+ return true;
+ }
+
+ char resolvedPath[512];
+ if (!N3DSRenderer_resolveAssetPath(renderer, relativePath, resolvedPath, sizeof(resolvedPath))) return false;
+
+ FILE* file = fopen(resolvedPath, "rb");
+ if (file == NULL) return false;
+ N3DSRenderer_configureFileBuffer(file);
+
+ fseek(file, 0, SEEK_END);
+ long sizeLong = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ if (sizeLong <= 0) {
+ fclose(file);
+ return false;
+ }
+
+ uint32_t size = (uint32_t) sizeLong;
+ while (renderer->cachedDirectT3xBytes + size > renderer->cachedDirectT3xByteLimit) {
+ if (!N3DSRenderer_evictLRUDirectAssetBlob(renderer, asset)) break;
+ }
+
+ uint8_t* blobData = safeMalloc((size_t) size);
+ if (blobData == NULL) {
+ fclose(file);
+ return false;
+ }
+
+ bool ok = fread(blobData, 1, (size_t) size, file) == (size_t) size;
+ fclose(file);
+ if (!ok) {
+ free(blobData);
+ return false;
+ }
+
+ asset->blobData = blobData;
+ asset->blobSize = size;
+ asset->blobLastUsedStamp = ++renderer->directBlobUseCounter;
+ renderer->cachedDirectT3xBytes += size;
+ renderer->frameDirectBlobReads++;
+ return true;
+}
+
+static bool N3DSRenderer_loadDirectTextureAsset(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath) {
+ if (renderer == NULL || asset == NULL || relativePath == NULL) return false;
+ if (asset->ready) {
+ asset->lastUsedStamp = ++renderer->directAssetUseCounter;
+ asset->lastUsedFrame = renderer->frameSequence;
+ if (asset->blobData != NULL) asset->blobLastUsedStamp = ++renderer->directBlobUseCounter;
+ return true;
+ }
+ if (asset->failed) return false;
+
+ if (!N3DSRenderer_ensureDirectTextureBlobLoaded(renderer, asset, relativePath)) {
+ asset->failed = true;
+ return false;
+ }
+
+ C2D_SpriteSheet sheet = C2D_SpriteSheetLoadFromMem(asset->blobData, asset->blobSize);
+ if (sheet == NULL) {
+ asset->failed = true;
+ return false;
+ }
+
+ C2D_Image image = C2D_SpriteSheetGetImage(sheet, 0);
+ if (image.tex == NULL || image.subtex == NULL) {
+ C2D_SpriteSheetFree(sheet);
+ asset->failed = true;
+ return false;
+ }
+
+ uint32_t vramBytes = (uint32_t) image.tex->width * (uint32_t) image.tex->height * 2u;
+ while (renderer->residentDirectAssetVRAMBytes + vramBytes > renderer->residentDirectAssetVRAMLimitBytes) {
+ if (!N3DSRenderer_evictLRUDirectAsset(renderer, asset)) break;
+ }
+
+ asset->sheet = sheet;
+ asset->image = image;
+ if (asset->image.tex != NULL) {
+ C3D_TexSetFilter(asset->image.tex, GPU_NEAREST, GPU_NEAREST);
+ }
+ asset->ready = true;
+ asset->failed = false;
+ asset->vramBytes = vramBytes;
+ asset->lastUsedStamp = ++renderer->directAssetUseCounter;
+ asset->lastUsedFrame = renderer->frameSequence;
+ renderer->residentDirectAssetVRAMBytes += vramBytes;
+ renderer->frameDirectAssetLoads++;
+ return true;
+}
+
+static void N3DSRenderer_applyBlendState(N3DSRenderer* renderer) {
+ if (!renderer->blendEnabled) {
+ C3D_AlphaBlend(GPU_BLEND_ADD, GPU_BLEND_ADD, GPU_ONE, GPU_ZERO, GPU_ONE, GPU_ZERO);
+ return;
+ }
+
+ GPU_BLENDEQUATION equation = GPU_BLEND_ADD;
+ switch (renderer->blendEquation) {
+ case bm_subtract: equation = GPU_BLEND_SUBTRACT; break;
+ case bm_reverse_subtract: equation = GPU_BLEND_REVERSE_SUBTRACT; break;
+ case bm_min: equation = GPU_BLEND_MIN; break;
+ case bm_max: equation = GPU_BLEND_MAX; break;
+ default: break;
+ }
+
+ GPU_BLENDFACTOR src = GPU_SRC_ALPHA;
+ GPU_BLENDFACTOR dst = GPU_ONE_MINUS_SRC_ALPHA;
+ switch (renderer->blendSrcFactor) {
+ case bm_zero: src = GPU_ZERO; break;
+ case bm_one: src = GPU_ONE; break;
+ case bm_src_color: src = GPU_SRC_COLOR; break;
+ case bm_inv_src_color: src = GPU_ONE_MINUS_SRC_COLOR; break;
+ case bm_dest_alpha: src = GPU_DST_ALPHA; break;
+ case bm_inv_dest_alpha: src = GPU_ONE_MINUS_DST_ALPHA; break;
+ case bm_dest_color: src = GPU_DST_COLOR; break;
+ case bm_inv_dest_color: src = GPU_ONE_MINUS_DST_COLOR; break;
+ case bm_src_alpha_sat: src = GPU_SRC_ALPHA_SATURATE; break;
+ default: src = GPU_SRC_ALPHA; break;
+ }
+ switch (renderer->blendDstFactor) {
+ case bm_zero: dst = GPU_ZERO; break;
+ case bm_one: dst = GPU_ONE; break;
+ case bm_src_color: dst = GPU_SRC_COLOR; break;
+ case bm_inv_src_color: dst = GPU_ONE_MINUS_SRC_COLOR; break;
+ case bm_src_alpha: dst = GPU_SRC_ALPHA; break;
+ case bm_dest_alpha: dst = GPU_DST_ALPHA; break;
+ case bm_inv_dest_alpha: dst = GPU_ONE_MINUS_DST_ALPHA; break;
+ case bm_dest_color: dst = GPU_DST_COLOR; break;
+ case bm_inv_dest_color: dst = GPU_ONE_MINUS_DST_COLOR; break;
+ case bm_src_alpha_sat: dst = GPU_SRC_ALPHA_SATURATE; break;
+ default: dst = GPU_ONE_MINUS_SRC_ALPHA; break;
+ }
+
+ C3D_AlphaBlend(equation, equation, src, dst, src, dst);
+}
+
+static void N3DSRenderer_applyAlphaState(N3DSRenderer* renderer) {
+ if (renderer->alphaTestEnabled) {
+ C3D_AlphaTest(true, GPU_GREATER, renderer->alphaTestRef);
+ } else {
+ C3D_AlphaTest(false, GPU_ALWAYS, 0);
+ }
+}
+
+static void N3DSRenderer_setDefaultGPUState(N3DSRenderer* renderer) {
+ if (renderer == NULL) return;
+ renderer->blendEnabled = true;
+ renderer->blendEquation = bm_normal;
+ renderer->blendSrcFactor = bm_src_alpha;
+ renderer->blendDstFactor = bm_inv_src_alpha;
+ renderer->alphaTestEnabled = false;
+ renderer->alphaTestRef = 0;
+ N3DSRenderer_applyBlendState(renderer);
+ N3DSRenderer_applyAlphaState(renderer);
+}
+
+static void N3DSRenderer_flushC2DQueue(N3DSRenderer* renderer) {
+ if (renderer == NULL || renderer->pendingC2DDraws == 0) return;
+ C2D_Flush();
+ renderer->pendingC2DDraws = 0;
+ renderer->lastDrawTexture = NULL;
+}
+
+static void N3DSRenderer_noteC2DDraws(N3DSRenderer* renderer, uint32_t drawCount) {
+ if (renderer == NULL || drawCount == 0) return;
+ renderer->pendingC2DDraws += drawCount;
+ if (renderer->pendingC2DDraws >= N3DS_C2D_FLUSH_DRAW_BUDGET) {
+ N3DSRenderer_flushC2DQueue(renderer);
+ }
+}
+
+static void N3DSRenderer_sceneBeginTarget(N3DSRenderer* renderer, uint8_t targetKind, bool force) {
+ if (renderer == NULL) return;
+ if (!force && renderer->activeSceneTarget == targetKind) return;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+
+ if (targetKind == N3DS_SCENE_TARGET_TOP) {
+ if (renderer->topTarget == NULL) return;
+ C2D_SceneBegin(renderer->topTarget);
+ renderer->activeSceneTarget = N3DS_SCENE_TARGET_TOP;
+ return;
+ }
+
+ if (targetKind == N3DS_SCENE_TARGET_BOTTOM) {
+ if (renderer->bottomTarget == NULL) return;
+ C2D_SceneBegin(renderer->bottomTarget);
+ renderer->activeSceneTarget = N3DS_SCENE_TARGET_BOTTOM;
+ return;
+ }
+
+ renderer->activeSceneTarget = N3DS_SCENE_TARGET_NONE;
+}
+
+static void N3DSRenderer_freeTileLayerChunkCache(N3DSRenderer* renderer, N3DSTileLayerChunkCache* cache) {
+ if (cache == NULL || cache->chunks == NULL) return;
+
+ repeat(cache->chunkCount, i) {
+ N3DSTileLayerChunk* chunk = &cache->chunks[i];
+ if (chunk->target != NULL) {
+ C3D_RenderTargetDelete(chunk->target);
+ chunk->target = NULL;
+ }
+ if (chunk->allocated) {
+ C3D_TexDelete(&chunk->texture);
+ memset(&chunk->texture, 0, sizeof(chunk->texture));
+ chunk->allocated = false;
+ }
+ }
+
+ free(cache->chunks);
+ cache->chunks = NULL;
+ cache->chunkCount = 0;
+ if (renderer != NULL) {
+ if (renderer->tileLayerChunkVRAMBytes >= cache->vramBytes) renderer->tileLayerChunkVRAMBytes -= cache->vramBytes;
+ else renderer->tileLayerChunkVRAMBytes = 0;
+ }
+ cache->vramBytes = 0;
+ cache->used = false;
+}
+
+static u32 N3DSRenderer_makeColor(uint32_t bgr, float alpha) {
+ return C2D_Color32(
+ (u8) BGR_R(bgr),
+ (u8) BGR_G(bgr),
+ (u8) BGR_B(bgr),
+ (u8) (C2D_Clamp(alpha, 0.0f, 1.0f) * 255.0f)
+ );
+}
+
+static bool N3DSRenderer_isIdentityTint(uint32_t color, float alpha) {
+ return color == 0x00FFFFFFu && alpha >= 0.999f;
+}
+
+static void N3DSRenderer_trackTextureUse(N3DSRenderer* renderer, const C2D_Image* image) {
+ if (renderer == NULL || image == NULL || image->tex == NULL) return;
+ if (renderer->lastDrawTexture != image->tex) {
+ renderer->frameTextureSwitches++;
+ renderer->lastDrawTexture = image->tex;
+ }
+}
+
+static void N3DSRenderer_applyTextureFilterForScale(N3DSRenderer* renderer, C3D_Tex* texture, float scaleX, float scaleY) {
+ if (texture == NULL || !Renderer_isFiniteFloat(scaleX) || !Renderer_isFiniteFloat(scaleY)) return;
+
+ if (renderer != NULL && renderer->textLinearFilterActive) {
+ C3D_TexSetFilter(texture, GPU_LINEAR, GPU_LINEAR);
+ return;
+ }
+
+ if (renderer != NULL && (renderer->bottomScreenGuiActive || renderer->topScreenGuiActive || renderer->topScreenBattleViewActive)) {
+ C3D_TexSetFilter(texture, GPU_NEAREST, GPU_NEAREST);
+ return;
+ }
+
+ GPU_TEXTURE_FILTER_PARAM minFilter = (fabsf(scaleX) < 0.999f || fabsf(scaleY) < 0.999f)
+ ? GPU_LINEAR
+ : GPU_NEAREST;
+ C3D_TexSetFilter(texture, minFilter, GPU_NEAREST);
+}
+
+static float N3DSRenderer_snapToPixel(float value) {
+ if (!Renderer_isFiniteFloat(value)) return value;
+ return floorf(value + 0.5f);
+}
+
+static float N3DSRenderer_transformScreenX(const N3DSRenderer* renderer, float localX) {
+ float screenX = renderer->frameOffsetX + renderer->portOffsetX + (localX - (float) renderer->viewX) * renderer->viewScaleX;
+ return N3DSRenderer_snapToPixel(screenX);
+}
+
+static float N3DSRenderer_transformScreenY(const N3DSRenderer* renderer, float localY) {
+ float screenY = renderer->frameOffsetY + renderer->portOffsetY + (localY - (float) renderer->viewY) * renderer->viewScaleY;
+ return N3DSRenderer_snapToPixel(screenY);
+}
+
+static void N3DSRenderer_transformScreenRect(
+ const N3DSRenderer* renderer,
+ float localX,
+ float localY,
+ float localW,
+ float localH,
+ float* outX,
+ float* outY,
+ float* outW,
+ float* outH
+) {
+ float screenX0 = N3DSRenderer_transformScreenX(renderer, localX);
+ float screenY0 = N3DSRenderer_transformScreenY(renderer, localY);
+ float screenX1 = N3DSRenderer_transformScreenX(renderer, localX + localW);
+ float screenY1 = N3DSRenderer_transformScreenY(renderer, localY + localH);
+
+ *outX = screenX0 < screenX1 ? screenX0 : screenX1;
+ *outY = screenY0 < screenY1 ? screenY0 : screenY1;
+ *outW = fabsf(screenX1 - screenX0);
+ *outH = fabsf(screenY1 - screenY0);
+}
+
+static void N3DSRenderer_drawImageFast(Renderer* base, C2D_Image* image, float localX, float localY, float xscale, float yscale, uint32_t color, float alpha) {
+ if (image == NULL || image->tex == NULL || image->subtex == NULL) return;
+ if (!Renderer_isFiniteFloat(localX) || !Renderer_isFiniteFloat(localY) ||
+ !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) ||
+ !Renderer_isFiniteFloat(alpha) || xscale == 0.0f || yscale == 0.0f) {
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ float localW = (float) image->subtex->width * xscale;
+ float localH = (float) image->subtex->height * yscale;
+ float screenX = 0.0f;
+ float screenY = 0.0f;
+ float screenW = 0.0f;
+ float screenH = 0.0f;
+ N3DSRenderer_transformScreenRect(renderer, localX, localY, localW, localH, &screenX, &screenY, &screenW, &screenH);
+ float screenScaleX = screenW / (float) image->subtex->width;
+ float screenScaleY = screenH / (float) image->subtex->height;
+ if (screenW <= 0.0f || screenH <= 0.0f) return;
+ if (N3DSRenderer_isScreenRectOffscreen(renderer, screenX, screenY, screenW, screenH)) return;
+ N3DSRenderer_trackTextureUse(renderer, image);
+ N3DSRenderer_applyTextureFilterForScale(renderer, image->tex, screenScaleX, screenScaleY);
+
+ if (N3DSRenderer_isIdentityTint(color, alpha)) {
+ C2D_DrawImageAt(*image, screenX, screenY, 0.5f, NULL, screenScaleX, screenScaleY);
+ } else {
+ C2D_ImageTint tint;
+ C2D_PlainImageTint(&tint, N3DSRenderer_makeColor(color, alpha), 1.0f);
+ C2D_DrawImageAt(*image, screenX, screenY, 0.5f, &tint, screenScaleX, screenScaleY);
+ }
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+}
+
+static bool N3DSRenderer_getSourceCropRect(
+ N3DSRenderer* renderer,
+ int32_t tpagIndex,
+ int32_t* outX,
+ int32_t* outY,
+ int32_t* outW,
+ int32_t* outH
+) {
+ if (renderer == NULL || outX == NULL || outY == NULL || outW == NULL || outH == NULL) return false;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->base.dataWin->tpag.count) return false;
+
+ TexturePageItem* tpag = &renderer->base.dataWin->tpag.items[tpagIndex];
+ if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ *outX = 0;
+ *outY = 0;
+ *outW = (int32_t) tpag->sourceWidth;
+ *outH = (int32_t) tpag->sourceHeight;
+ return *outW > 0 && *outH > 0;
+ }
+
+ if ((uint32_t) tpagIndex >= renderer->atlasItemCount) return false;
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ if (item->fragmentCount == 0) return false;
+
+ int32_t minX = INT32_MAX;
+ int32_t minY = INT32_MAX;
+ int32_t maxX = INT32_MIN;
+ int32_t maxY = INT32_MIN;
+
+ repeat(item->fragmentCount, fragIndex) {
+ uint32_t fragmentIndex = item->fragmentStart + (uint32_t) fragIndex;
+ if (fragmentIndex >= renderer->atlasFragmentCount) break;
+ N3DSAtlasFragment* fragment = &renderer->atlasFragments[fragmentIndex];
+ int32_t fragLeft = fragment->sourceX;
+ int32_t fragTop = fragment->sourceY;
+ int32_t fragRight = fragLeft + fragment->width;
+ int32_t fragBottom = fragTop + fragment->height;
+ if (minX > fragLeft) minX = fragLeft;
+ if (minY > fragTop) minY = fragTop;
+ if (maxX < fragRight) maxX = fragRight;
+ if (maxY < fragBottom) maxY = fragBottom;
+ }
+
+ if (minX >= maxX || minY >= maxY) return false;
+ *outX = minX;
+ *outY = minY;
+ *outW = maxX - minX;
+ *outH = maxY - minY;
+ return true;
+}
+
+static void N3DSRenderer_fillSubTexture(Tex3DS_SubTexture* subtex, const N3DSLoadedAtlasPage* page, uint16_t x, uint16_t y, uint16_t width, uint16_t height) {
+ subtex->width = width;
+ subtex->height = height;
+ subtex->left = (float) x / (float) page->width;
+ subtex->top = (float) (page->height - y) / (float) page->height;
+ subtex->right = (float) (x + width) / (float) page->width;
+ subtex->bottom = (float) (page->height - (y + height)) / (float) page->height;
+}
+
+static bool N3DSRenderer_resolveFragmentImage(N3DSRenderer* renderer, const N3DSAtlasFragment* fragment, C2D_Image* outImage, Tex3DS_SubTexture* subtex) {
+ if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) return false;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId];
+ if (!page->ready || fragment->width == 0 || fragment->height == 0) return false;
+
+ N3DSRenderer_fillSubTexture(subtex, page, fragment->x, fragment->y, fragment->width, fragment->height);
+ outImage->tex = &page->texture;
+ outImage->subtex = subtex;
+ return true;
+}
+
+static N3DSDynamicCaptureTPAG* N3DSRenderer_getDynamicCaptureTPAG(N3DSRenderer* renderer, int32_t tpagIndex) {
+ if (renderer == NULL) return NULL;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex < renderer->baseTPAGCount) return NULL;
+
+ uint32_t dynamicIndex = (uint32_t) tpagIndex - renderer->baseTPAGCount;
+ if (dynamicIndex >= renderer->dynamicCaptureTPAGCount) return NULL;
+ N3DSDynamicCaptureTPAG* capture = &renderer->dynamicCaptureTPAGs[dynamicIndex];
+ if (!capture->used || capture->image.tex == NULL || capture->image.subtex == NULL) return NULL;
+ return capture;
+}
+
+static void N3DSRenderer_freeDynamicCaptureTPAG(N3DSRenderer* renderer, N3DSDynamicCaptureTPAG* capture) {
+ if (renderer == NULL || capture == NULL || !capture->used) return;
+
+ C2D_Flush();
+ renderer->pendingC2DDraws = 0;
+ renderer->lastDrawTexture = NULL;
+ C3D_TexDelete(&capture->texture);
+ memset(capture, 0, sizeof(*capture));
+ capture->ownerSpriteIndex = -1;
+}
+
+static int32_t N3DSRenderer_allocDynamicCaptureTPAG(N3DSRenderer* renderer) {
+ if (renderer == NULL || renderer->base.dataWin == NULL) return -1;
+
+ repeat(renderer->dynamicCaptureTPAGCount, i) {
+ if (!renderer->dynamicCaptureTPAGs[i].used) {
+ renderer->dynamicCaptureTPAGs[i].ownerSpriteIndex = -1;
+ return (int32_t) (renderer->baseTPAGCount + (uint32_t) i);
+ }
+ }
+
+ uint32_t dynamicIndex = renderer->dynamicCaptureTPAGCount;
+ uint32_t newDynamicCount = dynamicIndex + 1u;
+ renderer->dynamicCaptureTPAGs = safeRealloc(
+ renderer->dynamicCaptureTPAGs,
+ (size_t) newDynamicCount * sizeof(N3DSDynamicCaptureTPAG)
+ );
+ memset(&renderer->dynamicCaptureTPAGs[dynamicIndex], 0, sizeof(renderer->dynamicCaptureTPAGs[dynamicIndex]));
+ renderer->dynamicCaptureTPAGs[dynamicIndex].ownerSpriteIndex = -1;
+ renderer->dynamicCaptureTPAGCount = newDynamicCount;
+
+ DataWin* dw = renderer->base.dataWin;
+ uint32_t newTPAGCount = renderer->baseTPAGCount + renderer->dynamicCaptureTPAGCount;
+ dw->tpag.items = safeRealloc(dw->tpag.items, (size_t) newTPAGCount * sizeof(TexturePageItem));
+ memset(&dw->tpag.items[newTPAGCount - 1u], 0, sizeof(TexturePageItem));
+ dw->tpag.count = newTPAGCount;
+ return (int32_t) (newTPAGCount - 1u);
+}
+
+static bool N3DSRenderer_tryLoadDirectSpriteImage(N3DSRenderer* renderer, int32_t spriteIndex, int32_t frameIndex, C2D_Image** outImage) {
+ if (renderer == NULL || outImage == NULL) return false;
+ if (spriteIndex < 0 || (uint32_t) spriteIndex >= renderer->directSpriteAssetCount) return false;
+
+ N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex];
+ if (frameIndex < 0 || (uint32_t) frameIndex >= spriteAsset->frameCount) return false;
+
+ N3DSDirectTextureAsset* sheetAsset = &spriteAsset->sheetAsset;
+ bool preferFrameFallback = (spriteIndex == 24);
+ const char* spriteName = NULL;
+ bool preferNamedButtonOverride = false;
+
+ if (renderer->base.dataWin != NULL &&
+ (uint32_t) spriteIndex < renderer->base.dataWin->sprt.count) {
+ spriteName = renderer->base.dataWin->sprt.sprites[spriteIndex].name;
+ preferNamedButtonOverride = Renderer_isBattleButtonSpriteName(spriteName);
+ if (spriteName != NULL &&
+ (strstr(spriteName, "spr_mainchara") != NULL ||
+ strstr(spriteName, "spr_f_mainchara") != NULL ||
+ strstr(spriteName, "spr_chara") != NULL ||
+ strstr(spriteName, "spr_heart") != NULL ||
+ strstr(spriteName, "heart_") != NULL ||
+ strstr(spriteName, "spr_soul") != NULL ||
+ strstr(spriteName, "soul") != NULL)) {
+ preferFrameFallback = true;
+ spriteAsset->useFrameFallback = true;
+ }
+ }
+
+ if (preferFrameFallback) spriteAsset->useFrameFallback = true;
+
+ bool hasLoadedDirectSheet = sheetAsset->ready || (sheetAsset->blobData != NULL && sheetAsset->blobSize > 0);
+ bool hasLoadedDirectFrame = false;
+ if (spriteAsset->frameAssets != NULL) {
+ N3DSDirectTextureAsset* existingFrameAsset = &spriteAsset->frameAssets[frameIndex];
+ hasLoadedDirectFrame = existingFrameAsset->ready || (existingFrameAsset->blobData != NULL && existingFrameAsset->blobSize > 0);
+ }
+ bool allowColdDirectLoad = preferNamedButtonOverride;
+ if (!allowColdDirectLoad && !hasLoadedDirectSheet && !hasLoadedDirectFrame) {
+ return false;
+ }
+
+ if (preferNamedButtonOverride && spriteAsset->frameAssets != NULL) {
+ N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex];
+ if (frameAsset->ready) {
+ frameAsset->lastUsedStamp = ++renderer->directAssetUseCounter;
+ frameAsset->lastUsedFrame = renderer->frameSequence;
+ *outImage = &frameAsset->image;
+ return true;
+ }
+ if (!frameAsset->failed && spriteName != NULL) {
+ char overridePath[256];
+ const char* overrideBaseName = spriteName;
+ if (strcmp(spriteName, "spr_talkbt") == 0) {
+ overrideBaseName = "spr_actbt_center";
+ } else if (strcmp(spriteName, "spr_talkbt_hollow") == 0) {
+ overrideBaseName = "spr_actbt_center_hole";
+ }
+ snprintf(overridePath, sizeof(overridePath), "button_overrides/%s_%d.t3x", overrideBaseName, (int) frameIndex);
+ if (N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, overridePath)) {
+ *outImage = &frameAsset->image;
+ return true;
+ }
+ snprintf(overridePath, sizeof(overridePath), "button_overrides/%s.t3x", overrideBaseName);
+ if (frameIndex == 0 && N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, overridePath)) {
+ *outImage = &frameAsset->image;
+ return true;
+ }
+ }
+ }
+
+ if (preferFrameFallback && spriteAsset->frameAssets != NULL) {
+ N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex];
+ if (frameAsset->ready) {
+ frameAsset->lastUsedStamp = ++renderer->directAssetUseCounter;
+ frameAsset->lastUsedFrame = renderer->frameSequence;
+ *outImage = &frameAsset->image;
+ return true;
+ }
+ if (!frameAsset->failed) {
+ char fallbackPath[256];
+ snprintf(fallbackPath, sizeof(fallbackPath), "sprites/spr_%05d_frame_%05d.t3x", (int) spriteIndex, (int) frameIndex);
+ if (N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, fallbackPath)) {
+ *outImage = &frameAsset->image;
+ return true;
+ }
+ }
+ }
+
+ if (sheetAsset->ready && (uint32_t) frameIndex < spriteAsset->sheetFrameCount) {
+ sheetAsset->lastUsedStamp = ++renderer->directAssetUseCounter;
+ sheetAsset->lastUsedFrame = renderer->frameSequence;
+ N3DSRenderer_buildSheetFrameImages(spriteAsset);
+ if (spriteAsset->sheetFrameImages != NULL) {
+ *outImage = &spriteAsset->sheetFrameImages[frameIndex];
+ return true;
+ }
+ *outImage = &sheetAsset->image;
+ return true;
+ }
+
+ if (!spriteAsset->sheetLoadAttempted) {
+ char path[256];
+ spriteAsset->sheetLoadAttempted = true;
+ snprintf(path, sizeof(path), "sprites/spr_%05d.t3x", (int) spriteIndex);
+ if (N3DSRenderer_loadDirectTextureAsset(renderer, sheetAsset, path) && sheetAsset->sheet != NULL) {
+ spriteAsset->sheetFrameCount = (uint32_t) C2D_SpriteSheetCount(sheetAsset->sheet);
+ if ((uint32_t) frameIndex < spriteAsset->sheetFrameCount) {
+ sheetAsset->lastUsedStamp = ++renderer->directAssetUseCounter;
+ sheetAsset->lastUsedFrame = renderer->frameSequence;
+ N3DSRenderer_buildSheetFrameImages(spriteAsset);
+ if (spriteAsset->sheetFrameImages != NULL) {
+ *outImage = &spriteAsset->sheetFrameImages[frameIndex];
+ return true;
+ }
+ *outImage = &sheetAsset->image;
+ return true;
+ }
+ }
+ spriteAsset->useFrameFallback = true;
+ }
+
+ if (!spriteAsset->useFrameFallback || spriteAsset->frameAssets == NULL) return false;
+
+ N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex];
+ if (frameAsset->ready) {
+ frameAsset->lastUsedStamp = ++renderer->directAssetUseCounter;
+ frameAsset->lastUsedFrame = renderer->frameSequence;
+ *outImage = &frameAsset->image;
+ return true;
+ }
+ if (frameAsset->failed) return false;
+
+ char fallbackPath[256];
+ snprintf(fallbackPath, sizeof(fallbackPath), "sprites/spr_%05d_frame_%05d.t3x", (int) spriteIndex, (int) frameIndex);
+ if (!N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, fallbackPath)) return false;
+ *outImage = &frameAsset->image;
+ return true;
+}
+
+static bool N3DSRenderer_tryLoadDirectBackgroundImage(N3DSRenderer* renderer, int32_t backgroundIndex, C2D_Image** outImage) {
+ if (renderer == NULL || outImage == NULL) return false;
+ if (backgroundIndex < 0 || (uint32_t) backgroundIndex >= renderer->directBackgroundAssetCount) return false;
+
+ N3DSDirectTextureAsset* asset = &renderer->directBackgroundAssets[backgroundIndex];
+ if (!asset->ready && (asset->blobData == NULL || asset->blobSize == 0)) {
+ return false;
+ }
+ char path[256];
+ snprintf(path, sizeof(path), "backgrounds/bg_%05d.t3x", (int) backgroundIndex);
+ if (!N3DSRenderer_loadDirectTextureAsset(renderer, asset, path)) return false;
+ *outImage = &asset->image;
+ return true;
+}
+
+static bool N3DSRenderer_tryResolveDirectFontImage(N3DSRenderer* renderer, int32_t fontIndex, C2D_Image* outImage, Tex3DS_SubTexture* outSubtex) {
+#if !N3DS_ENABLE_DIRECT_ASSETS
+ (void) renderer;
+ (void) fontIndex;
+ (void) outImage;
+ (void) outSubtex;
+ return false;
+#endif
+ if (renderer == NULL || outImage == NULL || outSubtex == NULL) return false;
+ if (fontIndex < 0 || (uint32_t) fontIndex >= renderer->directFontAssetCount) return false;
+
+ N3DSDirectTextureAsset* asset = &renderer->directFontAssets[fontIndex];
+ char path[256];
+ snprintf(path, sizeof(path), "fonts/font_%05d.t3x", (int) fontIndex);
+ if (!N3DSRenderer_loadDirectTextureAsset(renderer, asset, path)) return false;
+ if (asset->image.tex == NULL || asset->image.subtex == NULL) return false;
+
+ *outImage = asset->image;
+ *outSubtex = *asset->image.subtex;
+ outImage->subtex = outSubtex;
+ return true;
+}
+
+static bool N3DSRenderer_cropDirectSpriteImage(
+ const C2D_Image* directImage,
+ const TexturePageItem* tpag,
+ C2D_Image* outImage,
+ Tex3DS_SubTexture* outSubtex,
+ float* outDrawX,
+ float* outDrawY,
+ float x,
+ float y,
+ float originX,
+ float originY,
+ float xscale,
+ float yscale
+) {
+ if (directImage == NULL || directImage->tex == NULL || directImage->subtex == NULL ||
+ tpag == NULL || outImage == NULL || outSubtex == NULL || outDrawX == NULL || outDrawY == NULL) {
+ return false;
+ }
+
+ int32_t frameW = (int32_t) directImage->subtex->width;
+ int32_t frameH = (int32_t) directImage->subtex->height;
+ int32_t cropX = (int32_t) tpag->targetX;
+ int32_t cropY = (int32_t) tpag->targetY;
+ int32_t cropW = (int32_t) tpag->sourceWidth;
+ int32_t cropH = (int32_t) tpag->sourceHeight;
+ if (frameW <= 0 || frameH <= 0 || cropW <= 0 || cropH <= 0) return false;
+
+ int32_t drawTargetX = cropX;
+ int32_t drawTargetY = cropY;
+ if (cropX < 0) {
+ cropW += cropX;
+ cropX = 0;
+ drawTargetX = 0;
+ }
+ if (cropY < 0) {
+ cropH += cropY;
+ cropY = 0;
+ drawTargetY = 0;
+ }
+ if (cropX + cropW > frameW) cropW = frameW - cropX;
+ if (cropY + cropH > frameH) cropH = frameH - cropY;
+ if (cropW <= 0 || cropH <= 0) return false;
+
+ const Tex3DS_SubTexture* baseSubtex = directImage->subtex;
+ float baseW = (float) baseSubtex->width;
+ float baseH = (float) baseSubtex->height;
+ if (baseW <= 0.0f || baseH <= 0.0f) return false;
+
+ *outSubtex = *baseSubtex;
+ outSubtex->width = (uint16_t) cropW;
+ outSubtex->height = (uint16_t) cropH;
+ outSubtex->left = baseSubtex->left + (baseSubtex->right - baseSubtex->left) * ((float) cropX / baseW);
+ outSubtex->right = baseSubtex->left + (baseSubtex->right - baseSubtex->left) * ((float) (cropX + cropW) / baseW);
+ outSubtex->top = baseSubtex->top + (baseSubtex->bottom - baseSubtex->top) * ((float) cropY / baseH);
+ outSubtex->bottom = baseSubtex->top + (baseSubtex->bottom - baseSubtex->top) * ((float) (cropY + cropH) / baseH);
+
+ *outImage = *directImage;
+ outImage->subtex = outSubtex;
+ *outDrawX = x + ((float) drawTargetX - originX) * xscale;
+ *outDrawY = y + ((float) drawTargetY - originY) * yscale;
+ return true;
+}
+
+static bool N3DSRenderer_tryDrawDirectMappedSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) {
+#if !N3DS_ENABLE_DIRECT_ASSETS
+ (void) base;
+ (void) tpagIndex;
+ (void) x;
+ (void) y;
+ (void) originX;
+ (void) originY;
+ (void) xscale;
+ (void) yscale;
+ (void) angleDeg;
+ (void) color;
+ (void) alpha;
+ return false;
+#endif
+ if (base == NULL) return false;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ C2D_Image* directImage = NULL;
+ bool isBackgroundImage = false;
+
+ if (renderer->lastDirectTPAGIndex == tpagIndex &&
+ renderer->lastDirectTPAGImage != NULL &&
+ renderer->lastDirectTPAGImage->tex != NULL &&
+ renderer->lastDirectTPAGImage->subtex != NULL) {
+ directImage = renderer->lastDirectTPAGImage;
+ }
+
+ if (directImage == NULL &&
+ renderer->tpagToSpriteIndex != NULL && renderer->tpagToSpriteFrameIndex != NULL &&
+ tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) {
+ int32_t spriteIndex = renderer->tpagToSpriteIndex[tpagIndex];
+ int32_t frameIndex = renderer->tpagToSpriteFrameIndex[tpagIndex];
+ if (spriteIndex >= 0 && frameIndex >= 0 && N3DSRenderer_tryLoadDirectSpriteImage(renderer, spriteIndex, frameIndex, &directImage)) {
+ renderer->lastDirectTPAGIndex = tpagIndex;
+ renderer->lastDirectTPAGImage = directImage;
+ }
+ }
+
+ if (directImage == NULL &&
+ renderer->tpagToBackgroundIndex != NULL &&
+ tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) {
+ int32_t backgroundIndex = renderer->tpagToBackgroundIndex[tpagIndex];
+ if (backgroundIndex >= 0 && N3DSRenderer_tryLoadDirectBackgroundImage(renderer, backgroundIndex, &directImage)) {
+ isBackgroundImage = true;
+ renderer->lastDirectTPAGIndex = tpagIndex;
+ renderer->lastDirectTPAGImage = directImage;
+ }
+ }
+
+ if (directImage == NULL) return false;
+
+ if (!isBackgroundImage &&
+ renderer->tpagToBackgroundIndex != NULL &&
+ tpagIndex >= 0 &&
+ (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count &&
+ renderer->tpagToBackgroundIndex[tpagIndex] >= 0 &&
+ renderer->tpagToSpriteIndex[tpagIndex] < 0) {
+ isBackgroundImage = true;
+ }
+
+ C2D_Image croppedImage;
+ Tex3DS_SubTexture croppedSubtex;
+ C2D_Image* drawImage = directImage;
+ float drawX = x - originX * xscale;
+ float drawY = y - originY * yscale;
+ if (!isBackgroundImage && tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) {
+ TexturePageItem* tpag = &renderer->base.dataWin->tpag.items[tpagIndex];
+ if (N3DSRenderer_cropDirectSpriteImage(
+ directImage,
+ tpag,
+ &croppedImage,
+ &croppedSubtex,
+ &drawX,
+ &drawY,
+ x,
+ y,
+ originX,
+ originY,
+ xscale,
+ yscale)) {
+ drawImage = &croppedImage;
+ }
+ }
+
+ renderer->frameDirectSpriteHits++;
+ if (fabsf(angleDeg) < 0.001f && xscale > 0.0f && yscale > 0.0f) {
+ N3DSRenderer_drawImageFast(base, drawImage, drawX, drawY, xscale, yscale, color, alpha);
+ } else {
+ float pivotX = x - drawX;
+ float pivotY = y - drawY;
+ N3DSRenderer_drawImage(
+ base,
+ drawImage,
+ drawX,
+ drawY,
+ (float) drawImage->subtex->width * xscale,
+ (float) drawImage->subtex->height * yscale,
+ pivotX,
+ pivotY,
+ angleDeg,
+ color,
+ alpha
+ );
+ }
+ return true;
+}
+
+static bool N3DSRenderer_loadAtlas(N3DSRenderer* renderer) {
+ char assetPath[256];
+ FILE* atlasFile = N3DSRenderer_openAssetFile(renderer, "atlas.bin", assetPath, sizeof(assetPath));
+ if (atlasFile == NULL) {
+ N3DSRenderer_setStartupError(renderer, "Missing gfx/atlas.bin on SD or ROMFS");
+ fprintf(stderr, "N3DS: missing atlas.bin in sdmc:/3ds/cinnamon/gfx or romfs:/gfx\n");
+ return false;
+ }
+ fprintf(stderr, "N3DS: loading atlas from %s\n", assetPath);
+
+ fseek(atlasFile, 0, SEEK_END);
+ long atlasSize = ftell(atlasFile);
+ fseek(atlasFile, 0, SEEK_SET);
+ if (atlasSize < 24) {
+ fclose(atlasFile);
+ N3DSRenderer_setStartupError(renderer, "gfx/atlas.bin is too small or corrupt");
+ return false;
+ }
+
+ uint8_t header[24];
+ if (fread(header, 1, sizeof(header), atlasFile) != sizeof(header)) {
+ fclose(atlasFile);
+ N3DSRenderer_setStartupError(renderer, "Failed to read gfx/atlas.bin header");
+ return false;
+ }
+
+ uint16_t version = N3DS_readU16(header + 4);
+ if (N3DS_readU32(header) != N3DS_ATLAS_MAGIC ||
+ (version != N3DS_ATLAS_VERSION_RAW &&
+ version != N3DS_ATLAS_VERSION_T3X &&
+ version != N3DS_ATLAS_VERSION_FRAGMENTED &&
+ version != N3DS_ATLAS_VERSION_FRAGMENTED_TILES &&
+ version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS &&
+ version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT &&
+ version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT &&
+ version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED)) {
+ fclose(atlasFile);
+ N3DSRenderer_setStartupError(renderer, "gfx/atlas.bin has an invalid header");
+ fprintf(stderr, "N3DS: invalid atlas header\n");
+ return false;
+ }
+
+ renderer->atlasVersion = version;
+ renderer->atlasTextureFormat = N3DS_TEXFMT_RGBA5551;
+ renderer->atlasPageCount = N3DS_readU16(header + 6);
+ renderer->atlasItemCount = N3DS_readU32(header + 8);
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ renderer->atlasFragmentCount = N3DS_readU32(header + 12);
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ renderer->tileEntryCount = N3DS_readU32(header + 16);
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ renderer->atlasTextureFormat = N3DS_readU32(header + 20);
+ }
+ }
+ }
+ fprintf(stderr, "N3DS: atlas version=%u format=%s pages=%u items=%lu fragments=%lu tiles=%lu\n",
+ (unsigned int) renderer->atlasVersion,
+ N3DSRenderer_getTextureFormatName(renderer->atlasTextureFormat),
+ (unsigned int) renderer->atlasPageCount,
+ (unsigned long) renderer->atlasItemCount,
+ (unsigned long) renderer->atlasFragmentCount,
+ (unsigned long) renderer->tileEntryCount);
+
+ renderer->atlasPages = safeCalloc(renderer->atlasPageCount, sizeof(N3DSLoadedAtlasPage));
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ renderer->atlasItemsV3 = safeCalloc(renderer->atlasItemCount, sizeof(N3DSAtlasItemV3));
+ renderer->atlasFragments = safeCalloc(renderer->atlasFragmentCount, sizeof(N3DSAtlasFragment));
+ if (renderer->tileEntryCount > 0) {
+ renderer->tileEntries = safeCalloc(renderer->tileEntryCount, sizeof(N3DSTileAtlasEntry));
+ }
+ } else {
+ renderer->atlasItems = safeCalloc(renderer->atlasItemCount, sizeof(N3DSAtlasItem));
+ }
+
+ size_t cursor = 12;
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED) cursor = 16;
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS) cursor = 20;
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) cursor = 24;
+
+ size_t metadataSize = cursor;
+ if (version == N3DS_ATLAS_VERSION_RAW) metadataSize += (size_t) renderer->atlasPageCount * 12u;
+ else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT) metadataSize += (size_t) renderer->atlasPageCount * 8u;
+ else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) metadataSize += (size_t) renderer->atlasPageCount * 16u;
+ else metadataSize += (size_t) renderer->atlasPageCount * 4u;
+
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ metadataSize += (size_t) renderer->atlasItemCount * 10u;
+ metadataSize += (size_t) renderer->atlasFragmentCount * 14u;
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES) metadataSize += (size_t) renderer->tileEntryCount * 20u;
+ else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT ||
+ version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) metadataSize += (size_t) renderer->tileEntryCount * 26u;
+ } else {
+ metadataSize += (size_t) renderer->atlasItemCount * 10u;
+ }
+
+ if ((long) metadataSize > atlasSize) {
+ fclose(atlasFile);
+ N3DSRenderer_setStartupError(renderer, "gfx/atlas.bin metadata is truncated");
+ return false;
+ }
+
+ fseek(atlasFile, 0, SEEK_SET);
+ uint8_t* blob = safeMalloc(metadataSize);
+ if (fread(blob, 1, metadataSize, atlasFile) != metadataSize) {
+ free(blob);
+ fclose(atlasFile);
+ N3DSRenderer_setStartupError(renderer, "Failed to read gfx/atlas.bin metadata");
+ return false;
+ }
+
+ N3DSAtlasPageInfo* pageInfos = safeCalloc(renderer->atlasPageCount, sizeof(N3DSAtlasPageInfo));
+ repeat(renderer->atlasPageCount, i) {
+ pageInfos[i].width = N3DS_readU16(blob + cursor + 0);
+ pageInfos[i].height = N3DS_readU16(blob + cursor + 2);
+ pageInfos[i].textureFormat = renderer->atlasTextureFormat;
+ if (version == N3DS_ATLAS_VERSION_RAW) {
+ pageInfos[i].dataOffset = N3DS_readU32(blob + cursor + 4);
+ pageInfos[i].dataSize = N3DS_readU32(blob + cursor + 8);
+ cursor += 12;
+ } else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ pageInfos[i].textureFormat = N3DS_readU32(blob + cursor + 4);
+ pageInfos[i].dataOffset = N3DS_readU32(blob + cursor + 8);
+ pageInfos[i].dataSize = N3DS_readU32(blob + cursor + 12);
+ cursor += 16;
+ } else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT) {
+ pageInfos[i].textureFormat = N3DS_readU32(blob + cursor + 4);
+ cursor += 8;
+ } else {
+ cursor += 4;
+ }
+ }
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ repeat(renderer->atlasItemCount, i) {
+ renderer->atlasItemsV3[i].width = N3DS_readU16(blob + cursor + 0);
+ renderer->atlasItemsV3[i].height = N3DS_readU16(blob + cursor + 2);
+ renderer->atlasItemsV3[i].fragmentStart = N3DS_readU32(blob + cursor + 4);
+ renderer->atlasItemsV3[i].fragmentCount = N3DS_readU16(blob + cursor + 8);
+ cursor += 10;
+ }
+ repeat(renderer->atlasFragmentCount, i) {
+ renderer->atlasFragments[i].atlasId = N3DS_readU16(blob + cursor + 0);
+ renderer->atlasFragments[i].x = N3DS_readU16(blob + cursor + 2);
+ renderer->atlasFragments[i].y = N3DS_readU16(blob + cursor + 4);
+ renderer->atlasFragments[i].width = N3DS_readU16(blob + cursor + 6);
+ renderer->atlasFragments[i].height = N3DS_readU16(blob + cursor + 8);
+ renderer->atlasFragments[i].sourceX = N3DS_readU16(blob + cursor + 10);
+ renderer->atlasFragments[i].sourceY = N3DS_readU16(blob + cursor + 12);
+ cursor += 14;
+ }
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ repeat(renderer->tileEntryCount, i) {
+ renderer->tileEntries[i].bgDef = (int16_t) N3DS_readU16(blob + cursor + 0);
+ renderer->tileEntries[i].srcX = N3DS_readU16(blob + cursor + 2);
+ renderer->tileEntries[i].srcY = N3DS_readU16(blob + cursor + 4);
+ renderer->tileEntries[i].srcW = N3DS_readU16(blob + cursor + 6);
+ renderer->tileEntries[i].srcH = N3DS_readU16(blob + cursor + 8);
+ renderer->tileEntries[i].atlasId = N3DS_readU16(blob + cursor + 10);
+ renderer->tileEntries[i].x = N3DS_readU16(blob + cursor + 12);
+ renderer->tileEntries[i].y = N3DS_readU16(blob + cursor + 14);
+ renderer->tileEntries[i].width = N3DS_readU16(blob + cursor + 16);
+ renderer->tileEntries[i].height = N3DS_readU16(blob + cursor + 18);
+ if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ renderer->tileEntries[i].fragmentStart = N3DS_readU32(blob + cursor + 20);
+ renderer->tileEntries[i].fragmentCount = N3DS_readU16(blob + cursor + 24);
+ cursor += 26;
+ } else {
+ renderer->tileEntries[i].fragmentStart = UINT32_MAX;
+ renderer->tileEntries[i].fragmentCount = 0;
+ cursor += 20;
+ }
+ }
+ }
+ } else {
+ repeat(renderer->atlasItemCount, i) {
+ renderer->atlasItems[i].atlasId = N3DS_readU16(blob + cursor + 0);
+ renderer->atlasItems[i].x = N3DS_readU16(blob + cursor + 2);
+ renderer->atlasItems[i].y = N3DS_readU16(blob + cursor + 4);
+ renderer->atlasItems[i].width = N3DS_readU16(blob + cursor + 6);
+ renderer->atlasItems[i].height = N3DS_readU16(blob + cursor + 8);
+ cursor += 10;
+ }
+ }
+ free(blob);
+ if (N3DSRenderer_isPackedAtlasVersion(version)) renderer->packedAtlasFile = atlasFile;
+ else fclose(atlasFile);
+
+ repeat(renderer->atlasPageCount, i) {
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[i];
+ page->width = pageInfos[i].width;
+ page->height = pageInfos[i].height;
+ page->textureFormat = pageInfos[i].textureFormat;
+ page->dataOffset = pageInfos[i].dataOffset;
+ page->dataSize = pageInfos[i].dataSize;
+ }
+ free(pageInfos);
+ if (renderer->tileEntryCount > 0 && renderer->tileEntries != NULL) {
+ repeat(renderer->tileEntryCount, i) {
+ N3DSTileLookupKey key = {
+ .bgDef = renderer->tileEntries[i].bgDef,
+ .srcX = renderer->tileEntries[i].srcX,
+ .srcY = renderer->tileEntries[i].srcY,
+ .srcW = renderer->tileEntries[i].srcW,
+ .srcH = renderer->tileEntries[i].srcH,
+ };
+ hmput(renderer->tileEntryMap, key, (uint32_t) i);
+ }
+ }
+ fprintf(stderr, "N3DS: atlas metadata ready for %u pages; using lazy page loading (%u resident max)\n",
+ (unsigned int) renderer->atlasPageCount,
+ (unsigned int) renderer->residentAtlasPageLimit);
+
+ if (renderer->atlasTextureFormat == N3DS_TEXFMT_ETC1A4) {
+ renderer->residentAtlasPageLimit = renderer->isNew3DS ? N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS_ETC1A4 : N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS_ETC1A4;
+ renderer->prewarmGpuPageBudget = renderer->isNew3DS ? N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS_ETC1A4 : N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS_ETC1A4;
+ fprintf(stderr, "N3DS: ETC1A4 atlas detected; residentLimit=%u prewarm=%u\n",
+ (unsigned int) renderer->residentAtlasPageLimit,
+ (unsigned int) renderer->prewarmGpuPageBudget);
+ } else if (renderer->atlasTextureFormat == N3DS_TEXFMT_INDEXED8) {
+ fprintf(stderr, "N3DS: indexed8 atlas detected; using temporary CPU expansion to rgba5551 until shader-backed palette sampling lands\n");
+ } else if (renderer->atlasTextureFormat == N3DS_TEXFMT_HYBRID || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) {
+ renderer->residentAtlasPageLimit = renderer->isNew3DS ? N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS_ETC1A4 : N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS_ETC1A4;
+ renderer->prewarmGpuPageBudget = renderer->isNew3DS ? N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS_ETC1A4 : N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS_ETC1A4;
+ fprintf(stderr, "N3DS: hybrid atlas detected; mixed page formats enabled\n");
+ }
+ return true;
+}
+
+static bool N3DSRenderer_resolveImage(N3DSRenderer* renderer, int32_t tpagIndex, C2D_Image* outImage, Tex3DS_SubTexture* subtex) {
+ if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) return false;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false;
+ N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex];
+ if ((uint32_t) item->atlasId >= renderer->atlasPageCount) return false;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, item->atlasId)) return false;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[item->atlasId];
+ if (!page->ready || item->width == 0 || item->height == 0) return false;
+
+ N3DSRenderer_fillSubTexture(subtex, page, item->x, item->y, item->width, item->height);
+ outImage->tex = &page->texture;
+ outImage->subtex = subtex;
+ return true;
+}
+
+static bool N3DSRenderer_getPageIndexForTPAG(N3DSRenderer* renderer, int32_t tpagIndex, uint32_t* outPageIndex) {
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false;
+
+ if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ if (item->fragmentCount == 0) return false;
+ N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart];
+ if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false;
+ *outPageIndex = fragment->atlasId;
+ return true;
+ }
+
+ N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex];
+ if ((uint32_t) item->atlasId >= renderer->atlasPageCount) return false;
+ *outPageIndex = item->atlasId;
+ return true;
+}
+
+static N3DSTileAtlasEntry* N3DSRenderer_findTileEntryByKey(N3DSRenderer* renderer, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, uint32_t* outEntryIndex) {
+ if (renderer == NULL || renderer->tileEntryMap == NULL || renderer->tileEntries == NULL) return NULL;
+ if (backgroundIndex < 0 || srcW <= 0 || srcH <= 0) return NULL;
+
+ N3DSTileLookupKey key = {
+ .bgDef = (int16_t) backgroundIndex,
+ .srcX = (uint16_t) srcX,
+ .srcY = (uint16_t) srcY,
+ .srcW = (uint16_t) srcW,
+ .srcH = (uint16_t) srcH,
+ };
+ ptrdiff_t mapIndex = hmgeti(renderer->tileEntryMap, key);
+ if (mapIndex < 0) return NULL;
+ uint32_t entryIndex = renderer->tileEntryMap[mapIndex].value;
+ if (entryIndex >= renderer->tileEntryCount) return NULL;
+ if (outEntryIndex != NULL) *outEntryIndex = entryIndex;
+ return &renderer->tileEntries[entryIndex];
+}
+
+static N3DSTileAtlasEntry* N3DSRenderer_findTileEntry(N3DSRenderer* renderer, RoomTile* tile) {
+ if (renderer == NULL || tile == NULL) return NULL;
+ return N3DSRenderer_findTileEntryByKey(
+ renderer,
+ tile->backgroundDefinition,
+ tile->sourceX,
+ tile->sourceY,
+ (int32_t) tile->width,
+ (int32_t) tile->height,
+ NULL
+ );
+}
+
+static void N3DSRenderer_prewarmTileEntry(N3DSRenderer* renderer, bool* seenPages, RoomTile* tile) {
+ N3DSTileAtlasEntry* entry = N3DSRenderer_findTileEntry(renderer, tile);
+ if (entry == NULL) return;
+ if (entry->fragmentCount > 0) {
+ repeat(entry->fragmentCount, i) {
+ uint32_t fragmentIndex = entry->fragmentStart + (uint32_t) i;
+ if (fragmentIndex >= renderer->atlasFragmentCount) break;
+ N3DSRenderer_prewarmPage(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId);
+ }
+ } else if (entry->atlasId < renderer->atlasPageCount) {
+ N3DSRenderer_prewarmPage(renderer, seenPages, entry->atlasId);
+ }
+}
+
+static bool N3DSRenderer_prewarmPageBlobOnly(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex, uint32_t* remainingBlobBytes) {
+ if (renderer == NULL || seenPages == NULL) return false;
+ if (pageIndex >= renderer->atlasPageCount || seenPages[pageIndex]) return false;
+
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex];
+ seenPages[pageIndex] = true;
+
+ if (page->t3xData != NULL && page->t3xSize > 0) {
+ page->blobLastUsedStamp = ++renderer->blobUseCounter;
+ return true;
+ }
+
+ uint32_t blobSize = page->dataSize;
+ if (blobSize == 0 && page->t3xSize > 0) blobSize = page->t3xSize;
+ if (remainingBlobBytes != NULL && blobSize > 0 && *remainingBlobBytes < blobSize) {
+ return false;
+ }
+
+ if (!N3DSRenderer_ensurePageBlobLoaded(renderer, pageIndex)) return false;
+
+ if (remainingBlobBytes != NULL && blobSize > 0) {
+ if (*remainingBlobBytes > blobSize) *remainingBlobBytes -= blobSize;
+ else *remainingBlobBytes = 0;
+ }
+ return true;
+}
+
+static void N3DSRenderer_prewarmTileEntryBlobOnly(N3DSRenderer* renderer, bool* seenPages, const N3DSTileAtlasEntry* entry, uint32_t* remainingBlobBytes) {
+ if (renderer == NULL || seenPages == NULL || entry == NULL || remainingBlobBytes == NULL || *remainingBlobBytes == 0) return;
+
+ if (entry->fragmentCount > 0) {
+ repeat(entry->fragmentCount, i) {
+ uint32_t fragmentIndex = entry->fragmentStart + (uint32_t) i;
+ if (fragmentIndex >= renderer->atlasFragmentCount) break;
+ (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId, remainingBlobBytes);
+ if (*remainingBlobBytes == 0) break;
+ }
+ } else if (entry->atlasId < renderer->atlasPageCount) {
+ (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, entry->atlasId, remainingBlobBytes);
+ }
+}
+
+static void N3DSRenderer_prewarmTPAGBlobOnly(N3DSRenderer* renderer, bool* seenPages, int32_t tpagIndex, uint32_t* remainingBlobBytes) {
+ if (renderer == NULL || seenPages == NULL || remainingBlobBytes == NULL || *remainingBlobBytes == 0) return;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return;
+
+ if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ repeat(item->fragmentCount, i) {
+ uint32_t fragmentIndex = item->fragmentStart + (uint32_t) i;
+ if (fragmentIndex >= renderer->atlasFragmentCount) break;
+ (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId, remainingBlobBytes);
+ if (*remainingBlobBytes == 0) break;
+ }
+ return;
+ }
+
+ (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, renderer->atlasItems[tpagIndex].atlasId, remainingBlobBytes);
+}
+
+static void N3DSRenderer_prewarmPage(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex) {
+ if (pageIndex >= renderer->atlasPageCount || seenPages[pageIndex]) return;
+ seenPages[pageIndex] = true;
+ if (!N3DSRenderer_ensurePageBlobLoaded(renderer, pageIndex)) return;
+ if (renderer->residentAtlasPageCount < renderer->prewarmGpuPageBudget) {
+ (void) N3DSRenderer_ensurePageLoaded(renderer, pageIndex);
+ }
+}
+
+static void N3DSRenderer_prewarmTPAG(N3DSRenderer* renderer, bool* seenPages, int32_t tpagIndex) {
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return;
+
+ if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ repeat(item->fragmentCount, i) {
+ uint32_t fragmentIndex = item->fragmentStart + (uint32_t) i;
+ if (fragmentIndex >= renderer->atlasFragmentCount) break;
+ N3DSRenderer_prewarmPage(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId);
+ }
+ return;
+ }
+
+ N3DSRenderer_prewarmPage(renderer, seenPages, renderer->atlasItems[tpagIndex].atlasId);
+}
+
+static void N3DSRenderer_freeCachedTextLayout(N3DSCachedTextLayout* layout) {
+ free(layout->text);
+ free(layout->glyphs);
+ memset(layout, 0, sizeof(*layout));
+ layout->fontIndex = -1;
+}
+
+static void N3DSRenderer_appendCachedTextLayoutSuffix(N3DSCachedTextLayout* layout, Font* font, const char* suffix, int32_t suffixLen) {
+ if (suffixLen <= 0) return;
+
+ int32_t pos = 0;
+ float cursorX = layout->appendCursorX;
+ float cursorY = layout->appendCursorY;
+ uint16_t prevCodepoint = layout->appendPrevCodepoint;
+ bool atLineStart = layout->appendAtLineStart;
+
+ while (pos < suffixLen) {
+ if (TextUtils_isNewlineChar(suffix[pos])) {
+ pos = TextUtils_skipNewline(suffix, pos, suffixLen);
+ cursorX = 0.0f;
+ cursorY += TextUtils_lineStride(font);
+ prevCodepoint = 0;
+ atLineStart = true;
+ continue;
+ }
+
+ uint16_t ch = TextUtils_decodeUtf8(suffix, suffixLen, &pos);
+ FontGlyph* glyph = TextUtils_findGlyph(font, ch);
+ if (glyph == NULL) {
+ prevCodepoint = 0;
+ atLineStart = false;
+ continue;
+ }
+
+ if (!atLineStart && prevCodepoint != 0) {
+ FontGlyph* prevGlyph = TextUtils_findGlyph(font, prevCodepoint);
+ if (prevGlyph != NULL) {
+ cursorX += TextUtils_getKerningOffset(prevGlyph, ch);
+ }
+ }
+
+ if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) {
+ if (layout->glyphCount >= layout->glyphCapacity) {
+ layout->glyphCapacity = layout->glyphCapacity > 0 ? layout->glyphCapacity * 2 : 16;
+ layout->glyphs = safeRealloc(layout->glyphs, (size_t) layout->glyphCapacity * sizeof(N3DSCachedTextGlyph));
+ }
+
+ N3DSCachedTextGlyph* cachedGlyph = &layout->glyphs[layout->glyphCount++];
+ cachedGlyph->localX = cursorX + (float) glyph->offset;
+ cachedGlyph->localY = cursorY;
+ cachedGlyph->sourceX = glyph->sourceX;
+ cachedGlyph->sourceY = glyph->sourceY;
+ cachedGlyph->sourceWidth = glyph->sourceWidth;
+ cachedGlyph->sourceHeight = glyph->sourceHeight;
+ }
+
+ cursorX += (float) glyph->shift;
+ prevCodepoint = ch;
+ atLineStart = false;
+ }
+
+ layout->appendCursorX = cursorX;
+ layout->appendCursorY = cursorY;
+ layout->appendPrevCodepoint = prevCodepoint;
+ layout->appendAtLineStart = atLineStart;
+}
+
+static const N3DSCachedTextLayout* N3DSRenderer_getCachedTextLayout(N3DSRenderer* renderer, Font* font, int32_t fontIndex, const char* text) {
+ N3DSCachedTextLayout* layout = &renderer->cachedTextLayout;
+ if (layout->text != NULL &&
+ layout->fontIndex == fontIndex &&
+ layout->drawHalign == renderer->base.drawHalign &&
+ layout->drawValign == renderer->base.drawValign &&
+ strcmp(layout->text, text) == 0) {
+ return layout;
+ }
+
+ int32_t len = (int32_t) strlen(text);
+ bool canAppendSuffix =
+ layout->text != NULL &&
+ layout->fontIndex == fontIndex &&
+ layout->drawHalign == 0 &&
+ layout->drawValign == 0 &&
+ renderer->base.drawHalign == 0 &&
+ renderer->base.drawValign == 0 &&
+ len >= 0;
+
+ if (canAppendSuffix) {
+ int32_t cachedLen = (int32_t) strlen(layout->text);
+ canAppendSuffix =
+ len >= cachedLen &&
+ memcmp(text, layout->text, (size_t) cachedLen) == 0;
+
+ if (canAppendSuffix) {
+ if (len + 1 > layout->textCapacity) {
+ int32_t newCapacity = layout->textCapacity > 0 ? layout->textCapacity : 16;
+ while (newCapacity < len + 1) newCapacity *= 2;
+ layout->text = safeRealloc(layout->text, (size_t) newCapacity);
+ layout->textCapacity = newCapacity;
+ }
+
+ memcpy(layout->text + cachedLen, text + cachedLen, (size_t) (len - cachedLen + 1));
+ N3DSRenderer_appendCachedTextLayoutSuffix(layout, font, text + cachedLen, len - cachedLen);
+ return layout;
+ }
+ }
+
+ N3DSRenderer_freeCachedTextLayout(layout);
+ layout->fontIndex = fontIndex;
+ layout->drawHalign = renderer->base.drawHalign;
+ layout->drawValign = renderer->base.drawValign;
+ layout->textCapacity = len + 1;
+ layout->text = safeMalloc((size_t) layout->textCapacity);
+ memcpy(layout->text, text, (size_t) (len + 1));
+ layout->glyphCapacity = len > 0 ? len : 1;
+ layout->glyphs = safeCalloc((size_t) layout->glyphCapacity, sizeof(N3DSCachedTextGlyph));
+
+ int32_t lineCount = TextUtils_countLines(text, len);
+ float totalHeight = (float) lineCount * TextUtils_lineStride(font);
+ float valignOffset = 0.0f;
+ if (renderer->base.drawValign == 1) valignOffset = -totalHeight * 0.5f;
+ else if (renderer->base.drawValign == 2) valignOffset = -totalHeight;
+
+ layout->appendCursorX = 0.0f;
+ layout->appendCursorY = valignOffset - (float) font->ascenderOffset;
+ layout->appendPrevCodepoint = 0;
+ layout->appendAtLineStart = true;
+
+ if (renderer->base.drawHalign == 0 && renderer->base.drawValign == 0) {
+ layout->appendCursorY = -(float) font->ascenderOffset;
+ N3DSRenderer_appendCachedTextLayoutSuffix(layout, font, text, len);
+ return layout;
+ }
+
+ int32_t lineStart = 0;
+ while (lineStart <= len) {
+ int32_t lineEnd = lineStart;
+ while (lineEnd < len && !TextUtils_isNewlineChar(text[lineEnd])) lineEnd++;
+ float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineEnd - lineStart);
+ float cursorX = 0.0f;
+ if (renderer->base.drawHalign == 1) cursorX -= lineWidth * 0.5f;
+ else if (renderer->base.drawHalign == 2) cursorX -= lineWidth;
+
+ int32_t pos = lineStart;
+ while (pos < lineEnd) {
+ uint16_t ch = TextUtils_decodeUtf8(text, lineEnd, &pos);
+ FontGlyph* glyph = TextUtils_findGlyph(font, ch);
+ if (glyph == NULL) continue;
+
+ if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) {
+ N3DSCachedTextGlyph* cachedGlyph = &layout->glyphs[layout->glyphCount++];
+ cachedGlyph->localX = cursorX + (float) glyph->offset;
+ cachedGlyph->localY = layout->appendCursorY;
+ cachedGlyph->sourceX = glyph->sourceX;
+ cachedGlyph->sourceY = glyph->sourceY;
+ cachedGlyph->sourceWidth = glyph->sourceWidth;
+ cachedGlyph->sourceHeight = glyph->sourceHeight;
+ }
+
+ if (pos < lineEnd) {
+ int32_t previewPos = pos;
+ uint16_t nextCh = TextUtils_decodeUtf8(text, lineEnd, &previewPos);
+ cursorX += (float) glyph->shift + TextUtils_getKerningOffset(glyph, nextCh);
+ } else {
+ cursorX += (float) glyph->shift;
+ }
+ }
+
+ if (lineEnd >= len) break;
+ lineStart = TextUtils_skipNewline(text, lineEnd, len);
+ layout->appendCursorY += TextUtils_lineStride(font);
+ }
+
+ return layout;
+}
+
+static bool N3DSRenderer_tryDrawSingleGlyphTextFast(
+ Renderer* base,
+ N3DSRenderer* renderer,
+ Font* font,
+ const char* text,
+ int32_t len,
+ float x,
+ float y,
+ float effectiveXScale,
+ float effectiveYScale,
+ float angleDeg,
+ bool gradient,
+ int32_t c1,
+ float alpha,
+ bool useDirectFontAsset,
+ const C2D_Image* directFontImage,
+ const Tex3DS_SubTexture* directFontBaseSubtex,
+ const N3DSAtlasFragment* fontFragment,
+ N3DSLoadedAtlasPage* fontPage
+) {
+ if (text == NULL || len <= 0 || len > 4) return false;
+ if (TextUtils_isNewlineChar(text[0])) return false;
+
+ int32_t pos = 0;
+ uint16_t ch = TextUtils_decodeUtf8(text, len, &pos);
+ if (pos != len) return false;
+
+ float cursorX = 0.0f;
+ float lineWidth = TextUtils_measureLineWidth(font, text, len);
+ if (base->drawHalign == 1) cursorX -= lineWidth * 0.5f;
+ else if (base->drawHalign == 2) cursorX -= lineWidth;
+
+ float cursorY = 0.0f;
+ float totalHeight = TextUtils_lineStride(font);
+ if (base->drawValign == 1) cursorY -= totalHeight * 0.5f;
+ else if (base->drawValign == 2) cursorY -= totalHeight;
+ cursorY -= (float) font->ascenderOffset;
+
+ FontGlyph* glyph = TextUtils_findGlyph(font, ch);
+ if (glyph == NULL || glyph->sourceWidth == 0 || glyph->sourceHeight == 0) {
+ return true;
+ }
+
+ Tex3DS_SubTexture subtex;
+ C2D_Image image;
+ if (useDirectFontAsset) {
+ if (directFontImage == NULL || directFontBaseSubtex == NULL) return false;
+ subtex.width = glyph->sourceWidth;
+ subtex.height = glyph->sourceHeight;
+ float baseLeft = directFontBaseSubtex->left;
+ float baseRight = directFontBaseSubtex->right;
+ float baseTop = directFontBaseSubtex->top;
+ float baseBottom = directFontBaseSubtex->bottom;
+ float baseWidth = (float) directFontBaseSubtex->width;
+ float baseHeight = (float) directFontBaseSubtex->height;
+ subtex.left = baseLeft + (baseRight - baseLeft) * ((float) glyph->sourceX / baseWidth);
+ subtex.right = baseLeft + (baseRight - baseLeft) * (((float) glyph->sourceX + (float) glyph->sourceWidth) / baseWidth);
+ subtex.top = baseTop + (baseBottom - baseTop) * ((float) glyph->sourceY / baseHeight);
+ subtex.bottom = baseTop + (baseBottom - baseTop) * (((float) glyph->sourceY + (float) glyph->sourceHeight) / baseHeight);
+ image.tex = directFontImage->tex;
+ image.subtex = &subtex;
+ } else {
+ if (fontFragment == NULL || fontPage == NULL) return false;
+ image.tex = &fontPage->texture;
+ image.subtex = &subtex;
+ uint16_t px = (uint16_t) (fontFragment->x + glyph->sourceX);
+ uint16_t py = (uint16_t) (fontFragment->y + glyph->sourceY);
+ N3DSRenderer_fillSubTexture(&subtex, fontPage, px, py, glyph->sourceWidth, glyph->sourceHeight);
+ }
+
+ float drawX = x + (cursorX + (float) glyph->offset) * effectiveXScale;
+ float drawY = y + cursorY * effectiveYScale;
+ renderer->frameTextGlyphDraws++;
+ if (fabsf(angleDeg) < 0.001f) {
+ N3DSRenderer_drawImageFast(base, &image, drawX, drawY, effectiveXScale, effectiveYScale, gradient ? (uint32_t) c1 : base->drawColor, alpha);
+ } else {
+ N3DSRenderer_drawImage(
+ base,
+ &image,
+ drawX,
+ drawY,
+ (float) glyph->sourceWidth * effectiveXScale,
+ (float) glyph->sourceHeight * effectiveYScale,
+ 0.0f,
+ 0.0f,
+ angleDeg,
+ gradient ? (uint32_t) c1 : base->drawColor,
+ alpha
+ );
+ }
+
+ return true;
+}
+
+static void N3DSRenderer_preloadFontPages(N3DSRenderer* renderer) {
+ DataWin* dw = renderer->base.dataWin;
+ if (dw == NULL) return;
+
+ repeat(dw->font.count, i) {
+ Font* font = &dw->font.fonts[i];
+ if (font->isSpriteFont || font->tpagIndex < 0) continue;
+
+ uint32_t pageIndex = 0;
+ if (!N3DSRenderer_getPageIndexForTPAG(renderer, font->tpagIndex, &pageIndex)) continue;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, pageIndex)) continue;
+ renderer->atlasPages[pageIndex].pinned = true;
+ }
+}
+
+static void N3DSRenderer_prewarmRoomBlobCache(N3DSRenderer* renderer, Runner* runner) {
+ if (renderer == NULL || runner == NULL || runner->currentRoom == NULL || renderer->atlasPageCount == 0) return;
+
+ DataWin* dw = renderer->base.dataWin;
+ Room* room = runner->currentRoom;
+ uint32_t remainingBlobBytes = renderer->isNew3DS ? N3DS_PREWARM_BLOB_BYTES_NEW3DS : N3DS_PREWARM_BLOB_BYTES_OLD3DS;
+ bool* seenPages = safeCalloc(renderer->atlasPageCount, sizeof(bool));
+ uint32_t roomIndex = UINT32_MAX;
+
+ if (dw != NULL && dw->room.rooms != NULL && room >= dw->room.rooms && room < (dw->room.rooms + dw->room.count)) {
+ roomIndex = (uint32_t) (room - dw->room.rooms);
+ }
+
+ if (roomIndex != UINT32_MAX) {
+ N3DSRenderer_prewarmRoomManifestBlobCache(renderer, roomIndex, seenPages, &remainingBlobBytes);
+ if (remainingBlobBytes == 0) goto done;
+ }
+
+ repeat(8, i) {
+ RuntimeBackground* bg = &runner->backgrounds[i];
+ if (!bg->visible || bg->backgroundIndex < 0) continue;
+ N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, Renderer_resolveBackgroundTPAGIndex(dw, bg->backgroundIndex), &remainingBlobBytes);
+ if (remainingBlobBytes == 0) goto done;
+ }
+
+ repeat(room->layerCount, layerIndex) {
+ RoomLayer* layer = &room->layers[layerIndex];
+
+ if (layer->backgroundData != NULL && layer->backgroundData->visible && layer->backgroundData->spriteIndex >= 0) {
+ N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, Renderer_resolveSpriteTPAGIndex(dw, layer->backgroundData->spriteIndex), &remainingBlobBytes);
+ if (remainingBlobBytes == 0) goto done;
+ }
+
+ if (layer->assetsData != NULL) {
+ repeat(layer->assetsData->legacyTileCount, tileIndex) {
+ N3DSTileAtlasEntry* entry = N3DSRenderer_findTileEntry(renderer, &layer->assetsData->legacyTiles[tileIndex]);
+ N3DSRenderer_prewarmTileEntryBlobOnly(renderer, seenPages, entry, &remainingBlobBytes);
+ if (remainingBlobBytes == 0) goto done;
+ }
+
+ repeat(layer->assetsData->spriteCount, spriteIndex) {
+ SpriteInstance* sprite = &layer->assetsData->sprites[spriteIndex];
+ int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, sprite->spriteIndex, (int32_t) sprite->frameIndex);
+ N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, tpagIndex, &remainingBlobBytes);
+ if (remainingBlobBytes == 0) goto done;
+ }
+ }
+ }
+
+ repeat(room->tileCount, tileIndex) {
+ N3DSTileAtlasEntry* entry = N3DSRenderer_findTileEntry(renderer, &room->tiles[tileIndex]);
+ N3DSRenderer_prewarmTileEntryBlobOnly(renderer, seenPages, entry, &remainingBlobBytes);
+ if (remainingBlobBytes == 0) goto done;
+ }
+
+ if (runner->tileLayerCaches != NULL) {
+ repeat(runner->tileLayerCacheCount, cacheIndex) {
+ TileLayerRenderCache* cache = &runner->tileLayerCaches[cacheIndex];
+ if (!cache->built || cache->cells == NULL) continue;
+
+ repeat(arrlen(cache->cells), cellIndex) {
+ const TileLayerCacheCell* cell = &cache->cells[cellIndex];
+ if (cell->n3dsTileEntryIndex >= 0 && (uint32_t) cell->n3dsTileEntryIndex < renderer->tileEntryCount) {
+ N3DSRenderer_prewarmTileEntryBlobOnly(renderer, seenPages, &renderer->tileEntries[cell->n3dsTileEntryIndex], &remainingBlobBytes);
+ } else {
+ N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, cell->tpagIndex, &remainingBlobBytes);
+ }
+ if (remainingBlobBytes == 0) goto done;
+ }
+ }
+ }
+
+ repeat(arrlen(runner->instances), instanceIndex) {
+ Instance* inst = runner->instances[instanceIndex];
+ if (inst == NULL || inst->destroyed || !inst->visible || inst->spriteIndex < 0) continue;
+ int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, inst->spriteIndex, (int32_t) inst->imageIndex);
+ N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, tpagIndex, &remainingBlobBytes);
+ if (remainingBlobBytes == 0) goto done;
+ }
+
+done:
+ free(seenPages);
+}
+
+static void N3DSRenderer_prewarmRoom(Renderer* base, Runner* runner) {
+ if (base == NULL || runner == NULL || runner->currentRoom == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ Room* room = runner->currentRoom;
+ bool roomChanged = renderer->lastPrewarmedRoom != room;
+
+ N3DSRenderer_clearDirectAssetPins(renderer);
+ if (roomChanged) {
+ N3DSRenderer_flushRoomDirectAssets(renderer);
+ }
+ if (roomChanged && !renderer->isNew3DS) {
+ renderer->pendingOld3DSAtlasFlush = true;
+ }
+ renderer->lastPrewarmedRoom = room;
+ N3DSRenderer_prewarmRoomBlobCache(renderer, runner);
+}
+
+static void N3DSRenderer_computeFrameLayout(N3DSRenderer* renderer, int32_t gameW, int32_t gameH) {
+ N3DSRenderer_computeFrameLayoutForTarget(renderer, gameW, gameH, N3DS_TOP_WIDTH, N3DS_TOP_HEIGHT);
+}
+
+static void N3DSRenderer_init(Renderer* base, DataWin* dataWin) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ bool isNew3DS = false;
+ base->dataWin = dataWin;
+ renderer->baseTPAGCount = dataWin != NULL ? dataWin->tpag.count : 0u;
+ renderer->topTarget = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
+#ifndef N3DS_DISABLE_BOTTOM_SCREEN
+ renderer->bottomTarget = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);
+#endif
+ renderer->blendEnabled = true;
+ renderer->blendEquation = bm_normal;
+ renderer->blendSrcFactor = bm_src_alpha;
+ renderer->blendDstFactor = bm_inv_src_alpha;
+ renderer->clearColor = 0x000000;
+ renderer->clearAlpha = 1.0f;
+ if (R_SUCCEEDED(APT_CheckNew3DS(&isNew3DS))) {
+ renderer->isNew3DS = isNew3DS;
+ }
+#if N3DS_FORCE_OLD3DS_MODE
+ renderer->isNew3DS = false;
+#endif
+ renderer->residentAtlasPageLimit = renderer->isNew3DS ? N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS : N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS;
+ renderer->residentAtlasVRAMLimitBytes = renderer->isNew3DS ? N3DS_RESIDENT_ATLAS_VRAM_BUDGET_NEW3DS : N3DS_RESIDENT_ATLAS_VRAM_BUDGET_OLD3DS;
+ renderer->residentDirectAssetVRAMLimitBytes = renderer->isNew3DS ? N3DS_DIRECT_ASSET_VRAM_BUDGET_NEW3DS : N3DS_DIRECT_ASSET_VRAM_BUDGET_OLD3DS;
+ renderer->prewarmGpuPageBudget = renderer->isNew3DS ? N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS : N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS;
+ renderer->cachedT3xByteLimit = renderer->isNew3DS ? N3DS_MAX_CACHED_T3X_BYTES_NEW3DS : N3DS_MAX_CACHED_T3X_BYTES_OLD3DS;
+ renderer->cachedDirectT3xByteLimit = renderer->isNew3DS ? N3DS_MAX_CACHED_DIRECT_T3X_BYTES_NEW3DS : N3DS_MAX_CACHED_DIRECT_T3X_BYTES_OLD3DS;
+ renderer->atlasLoaded = N3DSRenderer_loadAtlas(renderer);
+ if (!renderer->atlasLoaded && renderer->startupError[0] == '\0') {
+ N3DSRenderer_setStartupError(renderer, "Failed to load 3DS graphics atlas");
+ }
+ N3DSRenderer_buildDirectAssetMaps(renderer);
+ (void) N3DSRenderer_loadPackedDirectAssets(renderer);
+ (void) N3DSRenderer_loadRoomManifest(renderer);
+ renderer->atlasTraceMask = safeCalloc(renderer->atlasItemCount > 0 ? renderer->atlasItemCount : 1u, sizeof(uint8_t));
+ renderer->atlasTraceFile = fopen(N3DS_ATLAS_TRACE_LOG_PATH, "wb");
+ if (renderer->atlasTraceFile != NULL) {
+ fprintf(renderer->atlasTraceFile, "N3DS atlas trace start\n");
+ fflush(renderer->atlasTraceFile);
+ }
+ N3DSRenderer_preloadFontPages(renderer);
+ C2D_SetTintMode(C2D_TintMult);
+ fprintf(
+ stderr,
+ "N3DSRenderer: model=%s residentLimit=%lu pages residentVRAM=%luKB directVRAM=%luKB prewarm=%lu cacheLimit=%luKB\n",
+ renderer->isNew3DS ? "new3ds" : "old3ds",
+ (unsigned long) renderer->residentAtlasPageLimit,
+ (unsigned long) (renderer->residentAtlasVRAMLimitBytes / 1024u),
+ (unsigned long) (renderer->residentDirectAssetVRAMLimitBytes / 1024u),
+ (unsigned long) renderer->prewarmGpuPageBudget,
+ (unsigned long) (renderer->cachedT3xByteLimit / 1024u)
+ );
+}
+
+static void N3DSRenderer_destroy(Renderer* base) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ repeat(renderer->atlasPageCount, i) {
+ N3DSRenderer_unloadPage(renderer, (uint32_t) i);
+ N3DSRenderer_unloadPageBlob(renderer, (uint32_t) i);
+ }
+ N3DSRenderer_freeCachedTextLayout(&renderer->cachedTextLayout);
+ repeat(shlen(renderer->resolvedAssetPathCache), i) {
+ free(renderer->resolvedAssetPathCache[i].value);
+ }
+ shfree(renderer->resolvedAssetPathCache);
+ shfree(renderer->packedDirectAssetMap);
+ free(renderer->roomManifestEntries);
+ free(renderer->roomManifestPageRefs);
+ free(renderer->atlasTraceMask);
+ if (renderer->atlasTraceFile != NULL) fclose(renderer->atlasTraceFile);
+ if (renderer->packedAtlasFile != NULL) fclose(renderer->packedAtlasFile);
+ if (renderer->packedDirectAssetFile != NULL) fclose(renderer->packedDirectAssetFile);
+ repeat(renderer->directSpriteAssetCount, spriteIndex) {
+ N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex];
+ N3DSRenderer_freeDirectTextureAsset(&spriteAsset->sheetAsset, renderer);
+ N3DSRenderer_unloadDirectTextureBlob(&spriteAsset->sheetAsset, renderer);
+ free(spriteAsset->sheetFrameImages);
+ spriteAsset->sheetFrameImages = NULL;
+ repeat(spriteAsset->frameCount, frameIndex) {
+ N3DSRenderer_freeDirectTextureAsset(&spriteAsset->frameAssets[frameIndex], renderer);
+ N3DSRenderer_unloadDirectTextureBlob(&spriteAsset->frameAssets[frameIndex], renderer);
+ }
+ free(spriteAsset->frameAssets);
+ }
+ repeat(renderer->directBackgroundAssetCount, bgIndex) {
+ N3DSRenderer_freeDirectTextureAsset(&renderer->directBackgroundAssets[bgIndex], renderer);
+ N3DSRenderer_unloadDirectTextureBlob(&renderer->directBackgroundAssets[bgIndex], renderer);
+ }
+ repeat(renderer->directFontAssetCount, fontIndex) {
+ N3DSRenderer_freeDirectTextureAsset(&renderer->directFontAssets[fontIndex], renderer);
+ N3DSRenderer_unloadDirectTextureBlob(&renderer->directFontAssets[fontIndex], renderer);
+ }
+ free(renderer->directSpriteAssets);
+ free(renderer->directBackgroundAssets);
+ free(renderer->directFontAssets);
+ if (renderer->dynamicCaptureTPAGs != NULL) {
+ repeat(renderer->dynamicCaptureTPAGCount, i) {
+ N3DSRenderer_freeDynamicCaptureTPAG(renderer, &renderer->dynamicCaptureTPAGs[i]);
+ }
+ free(renderer->dynamicCaptureTPAGs);
+ }
+ free(renderer->tpagToSpriteIndex);
+ free(renderer->tpagToSpriteFrameIndex);
+ free(renderer->tpagToBackgroundIndex);
+ free(renderer->atlasPages);
+ free(renderer->atlasItems);
+ free(renderer->atlasItemsV3);
+ free(renderer->atlasFragments);
+ free(renderer->tileEntries);
+ hmfree(renderer->tileEntryMap);
+ if (renderer->tileLayerChunkCaches != NULL) {
+ size_t chunkCacheCount = arrlenu(renderer->tileLayerChunkCaches);
+ repeat(chunkCacheCount, i) {
+ N3DSRenderer_freeTileLayerChunkCache(renderer, &renderer->tileLayerChunkCaches[i]);
+ }
+ arrfree(renderer->tileLayerChunkCaches);
+ renderer->tileLayerChunkCaches = NULL;
+ }
+ if (renderer->bottomTarget != NULL) {
+ C3D_RenderTargetDelete(renderer->bottomTarget);
+ }
+ if (renderer->topTarget != NULL) {
+ C3D_RenderTargetDelete(renderer->topTarget);
+ }
+ free(renderer);
+}
+
+static void N3DSRenderer_beginFrame(Renderer* base, int32_t gameW, int32_t gameH, MAYBE_UNUSED int32_t windowW, MAYBE_UNUSED int32_t windowH) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->frameSequence++;
+ renderer->activeSceneTarget = N3DS_SCENE_TARGET_NONE;
+ N3DSRenderer_resetFramePerfCounters(renderer);
+ N3DSRenderer_computeFrameLayout(renderer, gameW, gameH);
+
+ if (renderer->pendingOld3DSAtlasFlush && !renderer->isNew3DS) {
+ renderer->pendingOld3DSAtlasFlush = false;
+ N3DSRenderer_clearAtlasPagePins(renderer);
+ N3DSRenderer_preloadFontPages(renderer);
+ N3DSRenderer_flushRoomAtlasResidencyOld3DS(renderer);
+ }
+
+ N3DSRenderer_setDefaultGPUState(renderer);
+ if (renderer->bottomTarget != NULL) {
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_BOTTOM, true);
+ C2D_TargetClear(renderer->bottomTarget, C2D_Color32(0, 0, 0, 255));
+ }
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, true);
+ C2D_TargetClear(renderer->topTarget, N3DSRenderer_makeColor(renderer->clearColor, renderer->clearAlpha));
+}
+
+static void N3DSRenderer_endFrame(MAYBE_UNUSED Renderer* base) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ N3DSRenderer_logPerfWindowIfNeeded(renderer);
+ N3DSRenderer_flushC2DQueue(renderer);
+ C2D_Flush();
+ renderer->pendingC2DDraws = 0;
+ renderer->lastDrawTexture = NULL;
+}
+
+void N3DSRenderer_beginOverlay(Renderer* base) {
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ N3DSRenderer_flushC2DQueue(renderer);
+ N3DSRenderer_setDefaultGPUState(renderer);
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false);
+}
+
+void N3DSRenderer_beginBottomScreenGUIEx(Renderer* base, int32_t guiW, int32_t guiH, float scaleX, float scaleY, float offsetX, float offsetY) {
+#ifdef N3DS_DISABLE_BOTTOM_SCREEN
+ (void) base;
+ (void) guiW;
+ (void) guiH;
+ (void) scaleX;
+ (void) scaleY;
+ (void) offsetX;
+ (void) offsetY;
+ return;
+#endif
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (renderer->bottomTarget == NULL || renderer->bottomScreenGuiActive) return;
+
+ renderer->savedFrameScaleX = renderer->frameScaleX;
+ renderer->savedFrameScaleY = renderer->frameScaleY;
+ renderer->savedFrameOffsetX = renderer->frameOffsetX;
+ renderer->savedFrameOffsetY = renderer->frameOffsetY;
+ renderer->savedPortOffsetX = renderer->portOffsetX;
+ renderer->savedPortOffsetY = renderer->portOffsetY;
+ renderer->savedViewX = renderer->viewX;
+ renderer->savedViewY = renderer->viewY;
+ renderer->savedViewScaleX = renderer->viewScaleX;
+ renderer->savedViewScaleY = renderer->viewScaleY;
+
+ N3DSRenderer_computeFrameLayoutForTarget(renderer, guiW, guiH, N3DS_BOTTOM_WIDTH, N3DS_BOTTOM_HEIGHT);
+ renderer->viewX = 0;
+ renderer->viewY = 0;
+ renderer->viewScaleX = renderer->frameScaleX * scaleX;
+ renderer->viewScaleY = renderer->frameScaleY * scaleY;
+ renderer->portOffsetX = ((float) guiW * (renderer->frameScaleX - renderer->viewScaleX) * 0.5f) + offsetX;
+ renderer->portOffsetY = ((float) guiH * (renderer->frameScaleY - renderer->viewScaleY) * 0.5f) + offsetY;
+ renderer->bottomScreenGuiActive = true;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+ N3DSRenderer_setDefaultGPUState(renderer);
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_BOTTOM, false);
+}
+
+void N3DSRenderer_beginBottomScreenGUI(Renderer* base, int32_t guiW, int32_t guiH) {
+ N3DSRenderer_beginBottomScreenGUIEx(base, guiW, guiH, 1.0f, 1.0f, 0.0f, 0.0f);
+}
+
+void N3DSRenderer_beginBottomScreenGUIView(Renderer* base, int32_t guiW, int32_t guiH, int32_t viewX, int32_t viewY) {
+ N3DSRenderer_beginBottomScreenGUIEx(base, guiW, guiH, 1.0f, 1.0f, 0.0f, 0.0f);
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->viewX = viewX;
+ renderer->viewY = viewY;
+}
+
+void N3DSRenderer_endBottomScreenGUI(Renderer* base) {
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (!renderer->bottomScreenGuiActive) return;
+
+ renderer->frameScaleX = renderer->savedFrameScaleX;
+ renderer->frameScaleY = renderer->savedFrameScaleY;
+ renderer->frameOffsetX = renderer->savedFrameOffsetX;
+ renderer->frameOffsetY = renderer->savedFrameOffsetY;
+ renderer->portOffsetX = renderer->savedPortOffsetX;
+ renderer->portOffsetY = renderer->savedPortOffsetY;
+ renderer->viewX = renderer->savedViewX;
+ renderer->viewY = renderer->savedViewY;
+ renderer->viewScaleX = renderer->savedViewScaleX;
+ renderer->viewScaleY = renderer->savedViewScaleY;
+ renderer->bottomScreenGuiActive = false;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+ N3DSRenderer_applyBlendState(renderer);
+ N3DSRenderer_applyAlphaState(renderer);
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false);
+}
+
+void N3DSRenderer_beginBottomScreenGUI2x(Renderer* base, int32_t guiW, int32_t guiH) {
+ N3DSRenderer_beginBottomScreenGUIEx(base, guiW, guiH, 2.0f, 2.0f, 0.0f, 0.0f);
+}
+
+void N3DSRenderer_endBottomScreenGUI2x(Renderer* base) {
+ N3DSRenderer_endBottomScreenGUI(base);
+}
+
+void N3DSRenderer_beginTopScreenGUI(Renderer* base, int32_t guiW, int32_t guiH) {
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (renderer->topScreenGuiActive || renderer->bottomScreenGuiActive) return;
+
+ renderer->savedFrameScaleX = renderer->frameScaleX;
+ renderer->savedFrameScaleY = renderer->frameScaleY;
+ renderer->savedFrameOffsetX = renderer->frameOffsetX;
+ renderer->savedFrameOffsetY = renderer->frameOffsetY;
+ renderer->savedPortOffsetX = renderer->portOffsetX;
+ renderer->savedPortOffsetY = renderer->portOffsetY;
+ renderer->savedViewX = renderer->viewX;
+ renderer->savedViewY = renderer->viewY;
+ renderer->savedViewScaleX = renderer->viewScaleX;
+ renderer->savedViewScaleY = renderer->viewScaleY;
+
+ N3DSRenderer_setTopBattle320x240Layout(renderer, guiW, guiH, N3DS_TOP_BATTLE_SCENE_Y_OFFSET);
+ renderer->topScreenGuiActive = true;
+ renderer->topScreenGui2xActive = false;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+ N3DSRenderer_setDefaultGPUState(renderer);
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false);
+}
+
+void N3DSRenderer_endTopScreenGUI(Renderer* base) {
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (!renderer->topScreenGuiActive) return;
+
+ renderer->frameScaleX = renderer->savedFrameScaleX;
+ renderer->frameScaleY = renderer->savedFrameScaleY;
+ renderer->frameOffsetX = renderer->savedFrameOffsetX;
+ renderer->frameOffsetY = renderer->savedFrameOffsetY;
+ renderer->portOffsetX = renderer->savedPortOffsetX;
+ renderer->portOffsetY = renderer->savedPortOffsetY;
+ renderer->viewX = renderer->savedViewX;
+ renderer->viewY = renderer->savedViewY;
+ renderer->viewScaleX = renderer->savedViewScaleX;
+ renderer->viewScaleY = renderer->savedViewScaleY;
+ renderer->topScreenGuiActive = false;
+ renderer->topScreenGui2xActive = false;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+ N3DSRenderer_applyBlendState(renderer);
+ N3DSRenderer_applyAlphaState(renderer);
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false);
+}
+
+void N3DSRenderer_beginTopScreenGUI2x(Renderer* base, int32_t guiW, int32_t guiH) {
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (renderer->topScreenGuiActive || renderer->bottomScreenGuiActive) return;
+
+ renderer->savedFrameScaleX = renderer->frameScaleX;
+ renderer->savedFrameScaleY = renderer->frameScaleY;
+ renderer->savedFrameOffsetX = renderer->frameOffsetX;
+ renderer->savedFrameOffsetY = renderer->frameOffsetY;
+ renderer->savedPortOffsetX = renderer->portOffsetX;
+ renderer->savedPortOffsetY = renderer->portOffsetY;
+ renderer->savedViewX = renderer->viewX;
+ renderer->savedViewY = renderer->viewY;
+ renderer->savedViewScaleX = renderer->viewScaleX;
+ renderer->savedViewScaleY = renderer->viewScaleY;
+
+ N3DSRenderer_setTopBattle320x240Layout(renderer, guiW, guiH, N3DS_TOP_BATTLE_ENEMY_Y_OFFSET);
+ renderer->viewScaleX *= 2.0f;
+ renderer->viewScaleY *= 2.0f;
+ renderer->topScreenGuiActive = true;
+ renderer->topScreenGui2xActive = true;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+ N3DSRenderer_setDefaultGPUState(renderer);
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false);
+}
+
+void N3DSRenderer_endTopScreenGUI2x(Renderer* base) {
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (!renderer->topScreenGuiActive) return;
+
+ renderer->frameScaleX = renderer->savedFrameScaleX;
+ renderer->frameScaleY = renderer->savedFrameScaleY;
+ renderer->frameOffsetX = renderer->savedFrameOffsetX;
+ renderer->frameOffsetY = renderer->savedFrameOffsetY;
+ renderer->portOffsetX = renderer->savedPortOffsetX;
+ renderer->portOffsetY = renderer->savedPortOffsetY;
+ renderer->viewX = renderer->savedViewX;
+ renderer->viewY = renderer->savedViewY;
+ renderer->viewScaleX = renderer->savedViewScaleX;
+ renderer->viewScaleY = renderer->savedViewScaleY;
+ renderer->topScreenGuiActive = false;
+ renderer->topScreenGui2xActive = false;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+ N3DSRenderer_applyBlendState(renderer);
+ N3DSRenderer_applyAlphaState(renderer);
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false);
+}
+
+bool N3DSRenderer_isTopScreenGUIActive(Renderer* base) {
+ if (base == NULL) return false;
+ return ((N3DSRenderer*) base)->topScreenGuiActive;
+}
+
+bool N3DSRenderer_isTopScreenBattleViewActive(Renderer* base) {
+ if (base == NULL) return false;
+ return ((N3DSRenderer*) base)->topScreenBattleViewActive;
+}
+
+void N3DSRenderer_setTopScreenBattleViewActive(Renderer* base, bool active) {
+ if (base == NULL) return;
+ ((N3DSRenderer*) base)->topScreenBattleViewActive = active;
+}
+
+static void N3DSRenderer_beginView(Renderer* base, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, MAYBE_UNUSED float viewAngle) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->viewX = viewX;
+ renderer->viewY = viewY;
+ renderer->portOffsetX = (float) portX * renderer->frameScaleX;
+ renderer->portOffsetY = (float) portY * renderer->frameScaleY;
+ renderer->viewScaleX = viewW > 0 ? ((float) portW / (float) viewW) * renderer->frameScaleX : renderer->frameScaleX;
+ renderer->viewScaleY = viewH > 0 ? ((float) portH / (float) viewH) * renderer->frameScaleY : renderer->frameScaleY;
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false);
+}
+
+static void N3DSRenderer_endView(MAYBE_UNUSED Renderer* base) {}
+
+static void N3DSRenderer_beginGUI(Renderer* base, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH) {
+ N3DSRenderer_beginView(base, 0, 0, guiW, guiH, portX, portY, portW, portH, 0.0f);
+}
+
+static void N3DSRenderer_endGUI(Renderer* base) {
+ N3DSRenderer_endView(base);
+}
+
+static void N3DSRenderer_drawImage(Renderer* base, C2D_Image* image, float localX, float localY, float width, float height, float pivotX, float pivotY, float angleDeg, uint32_t color, float alpha) {
+ if (image == NULL || image->tex == NULL || image->subtex == NULL) return;
+ if (!Renderer_isFiniteFloat(localX) || !Renderer_isFiniteFloat(localY) ||
+ !Renderer_isFiniteFloat(width) || !Renderer_isFiniteFloat(height) ||
+ !Renderer_isFiniteFloat(pivotX) || !Renderer_isFiniteFloat(pivotY) ||
+ !Renderer_isFiniteFloat(angleDeg) || !Renderer_isFiniteFloat(alpha) ||
+ width == 0.0f || height == 0.0f ||
+ image->subtex->width == 0.0f || image->subtex->height == 0.0f) {
+ return;
+ }
+
+ bool flipX = false;
+ bool flipY = false;
+ if (width < 0.0f) {
+ localX += width;
+ pivotX -= width;
+ width = -width;
+ flipX = true;
+ }
+ if (height < 0.0f) {
+ localY += height;
+ pivotY -= height;
+ height = -height;
+ flipY = true;
+ }
+
+ Tex3DS_SubTexture flippedSubtex;
+ C2D_Image drawImage = *image;
+ if (flipX || flipY) {
+ flippedSubtex = *image->subtex;
+ if (flipX) {
+ float tmp = flippedSubtex.left;
+ flippedSubtex.left = flippedSubtex.right;
+ flippedSubtex.right = tmp;
+ }
+ if (flipY) {
+ float tmp = flippedSubtex.top;
+ flippedSubtex.top = flippedSubtex.bottom;
+ flippedSubtex.bottom = tmp;
+ }
+ drawImage.subtex = &flippedSubtex;
+ }
+
+ if (fabsf(angleDeg) < 0.001f && fabsf(pivotX) < 0.001f && fabsf(pivotY) < 0.001f) {
+ float xscale = width / (float) drawImage.subtex->width;
+ float yscale = height / (float) drawImage.subtex->height;
+ N3DSRenderer_drawImageFast(base, &drawImage, localX, localY, xscale, yscale, color, alpha);
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ float screenRectX = 0.0f;
+ float screenRectY = 0.0f;
+ float screenRectW = 0.0f;
+ float screenRectH = 0.0f;
+ N3DSRenderer_transformScreenRect(renderer, localX, localY, width, height, &screenRectX, &screenRectY, &screenRectW, &screenRectH);
+ if (screenRectW <= 0.0f || screenRectH <= 0.0f) return;
+ if (N3DSRenderer_isScreenRotatedRectOffscreen(renderer, screenRectX, screenRectY, screenRectW, screenRectH, angleDeg)) {
+ return;
+ }
+
+ float pivotLocalX = localX + pivotX;
+ float pivotLocalY = localY + pivotY;
+ float screenX = N3DSRenderer_transformScreenX(renderer, pivotLocalX);
+ float screenY = N3DSRenderer_transformScreenY(renderer, pivotLocalY);
+ float screenScaleX = screenRectW / (float) drawImage.subtex->width;
+ float screenScaleY = screenRectH / (float) drawImage.subtex->height;
+ float screenPivotX = (width != 0.0f) ? (pivotX / width) * screenRectW : 0.0f;
+ float screenPivotY = (height != 0.0f) ? (pivotY / height) * screenRectH : 0.0f;
+
+ C2D_DrawParams params = {
+ .pos = { screenX, screenY, screenRectW, screenRectH },
+ .center = { screenPivotX, screenPivotY },
+ .depth = 0.5f,
+ .angle = C3D_AngleFromDegrees(-angleDeg),
+ };
+ N3DSRenderer_trackTextureUse(renderer, &drawImage);
+ N3DSRenderer_applyTextureFilterForScale(renderer, drawImage.tex, screenScaleX, screenScaleY);
+ if (N3DSRenderer_isIdentityTint(color, alpha)) {
+ C2D_DrawImage(drawImage, ¶ms, NULL);
+ } else {
+ C2D_ImageTint tint;
+ C2D_PlainImageTint(&tint, N3DSRenderer_makeColor(color, alpha), 1.0f);
+ C2D_DrawImage(drawImage, ¶ms, &tint);
+ }
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+}
+
+static bool N3DSRenderer_tryResolveSingleFragmentFontPage(N3DSRenderer* renderer, int32_t tpagIndex, const N3DSAtlasFragment** outFragment, N3DSLoadedAtlasPage** outPage) {
+ if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) return false;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false;
+
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ if (item->fragmentCount != 1) return false;
+
+ const N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart];
+ if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) return false;
+
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId];
+ if (!page->ready) return false;
+
+ *outFragment = fragment;
+ *outPage = page;
+ return true;
+}
+
+static void N3DSRenderer_drawSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) {
+ if (!Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) ||
+ !Renderer_isFiniteFloat(originX) || !Renderer_isFiniteFloat(originY) ||
+ !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) ||
+ !Renderer_isFiniteFloat(angleDeg) || !Renderer_isFiniteFloat(alpha) ||
+ xscale == 0.0f || yscale == 0.0f) {
+ return;
+ }
+
+ DataWin* dw = base->dataWin;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) return;
+ TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ N3DSDynamicCaptureTPAG* dynamicCapture = N3DSRenderer_getDynamicCaptureTPAG(renderer, tpagIndex);
+ N3DSRenderer_traceTPAGUsage(renderer, N3DS_TRACE_KIND_SPRITE, tpagIndex);
+ renderer->frameSpriteDrawCalls++;
+ if (dynamicCapture != NULL) {
+ float localX = ((float) tpag->targetX - originX) * xscale;
+ float localY = ((float) tpag->targetY - originY) * yscale;
+ float drawX = x + localX;
+ float drawY = y + localY;
+ N3DSRenderer_drawImage(
+ base,
+ &dynamicCapture->image,
+ drawX,
+ drawY,
+ (float) tpag->sourceWidth * xscale,
+ (float) tpag->sourceHeight * yscale,
+ -localX,
+ -localY,
+ angleDeg,
+ color,
+ alpha
+ );
+ return;
+ }
+ if (N3DSRenderer_tryDrawDirectMappedSprite(base, tpagIndex, x, y, originX, originY, xscale, yscale, angleDeg, color, alpha)) {
+ return;
+ }
+ if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ if ((uint32_t) tpagIndex >= renderer->atlasItemCount) return;
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ repeat(item->fragmentCount, fragIndex) {
+ N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart + fragIndex];
+ C2D_Image image;
+ Tex3DS_SubTexture subtex;
+ if (!N3DSRenderer_resolveFragmentImage(renderer, fragment, &image, &subtex)) continue;
+ renderer->frameFragmentDraws++;
+ float localX = ((float) tpag->targetX + (float) fragment->sourceX - originX) * xscale;
+ float localY = ((float) tpag->targetY + (float) fragment->sourceY - originY) * yscale;
+ float drawX = x + localX;
+ float drawY = y + localY;
+ N3DSRenderer_drawImage(
+ base,
+ &image,
+ drawX,
+ drawY,
+ (float) fragment->width * xscale,
+ (float) fragment->height * yscale,
+ -localX,
+ -localY,
+ angleDeg,
+ color,
+ alpha
+ );
+ }
+ return;
+ }
+
+ C2D_Image image;
+ Tex3DS_SubTexture subtex;
+ if (!N3DSRenderer_resolveImage(renderer, tpagIndex, &image, &subtex)) return;
+
+ float localX = ((float) tpag->targetX - originX) * xscale;
+ float localY = ((float) tpag->targetY - originY) * yscale;
+ float drawX = x + localX;
+ float drawY = y + localY;
+ N3DSRenderer_drawImage(
+ base,
+ &image,
+ drawX,
+ drawY,
+ (float) tpag->sourceWidth * xscale,
+ (float) tpag->sourceHeight * yscale,
+ -localX,
+ -localY,
+ angleDeg,
+ color,
+ alpha
+ );
+}
+
+static void N3DSRenderer_drawSpritePart(Renderer* base, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) {
+ if (srcW <= 0 || srcH <= 0) return;
+ if (!Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) ||
+ !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) ||
+ !Renderer_isFiniteFloat(angleDeg) || !Renderer_isFiniteFloat(pivotX) ||
+ !Renderer_isFiniteFloat(pivotY) || !Renderer_isFiniteFloat(alpha) ||
+ xscale == 0.0f || yscale == 0.0f) {
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ N3DSDynamicCaptureTPAG* dynamicCapture = N3DSRenderer_getDynamicCaptureTPAG(renderer, tpagIndex);
+ N3DSRenderer_traceTPAGUsage(renderer, N3DS_TRACE_KIND_SPRITE_PART, tpagIndex);
+ renderer->frameSpritePartDrawCalls++;
+ if (dynamicCapture != NULL) {
+ DataWin* dw = base->dataWin;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) return;
+ TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
+ float logicalW = (float) (tpag->sourceWidth > 0 ? tpag->sourceWidth : dynamicCapture->logicalWidth);
+ float logicalH = (float) (tpag->sourceHeight > 0 ? tpag->sourceHeight : dynamicCapture->logicalHeight);
+ float texW = (float) dynamicCapture->subtex.width;
+ float texH = (float) dynamicCapture->subtex.height;
+ if (logicalW <= 0.0f || logicalH <= 0.0f || texW <= 0.0f || texH <= 0.0f) return;
+
+ Tex3DS_SubTexture subtex = dynamicCapture->subtex;
+ float u0 = (float) srcOffX / logicalW;
+ float v0 = (float) srcOffY / logicalH;
+ float u1 = (float) (srcOffX + srcW) / logicalW;
+ float v1 = (float) (srcOffY + srcH) / logicalH;
+ if (u0 < 0.0f) u0 = 0.0f;
+ if (v0 < 0.0f) v0 = 0.0f;
+ if (u1 > 1.0f) u1 = 1.0f;
+ if (v1 > 1.0f) v1 = 1.0f;
+ if (u0 >= u1 || v0 >= v1) return;
+
+ float left = dynamicCapture->subtex.left;
+ float right = dynamicCapture->subtex.right;
+ float top = dynamicCapture->subtex.top;
+ float bottom = dynamicCapture->subtex.bottom;
+ subtex.left = left + (right - left) * u0;
+ subtex.right = left + (right - left) * u1;
+ subtex.top = top + (bottom - top) * v0;
+ subtex.bottom = top + (bottom - top) * v1;
+ subtex.width = (uint16_t) lroundf((u1 - u0) * texW);
+ subtex.height = (uint16_t) lroundf((v1 - v0) * texH);
+ if (subtex.width == 0 || subtex.height == 0) return;
+
+ C2D_Image image = {
+ .tex = dynamicCapture->image.tex,
+ .subtex = &subtex,
+ };
+ N3DSRenderer_drawImage(
+ base,
+ &image,
+ x,
+ y,
+ (float) srcW * xscale,
+ (float) srcH * yscale,
+ pivotX - x,
+ pivotY - y,
+ angleDeg,
+ color,
+ alpha
+ );
+ return;
+ }
+
+ C2D_Image* directImage = NULL;
+ if (renderer->tpagToSpriteIndex != NULL && renderer->tpagToSpriteFrameIndex != NULL &&
+ tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) {
+ int32_t spriteIndex = renderer->tpagToSpriteIndex[tpagIndex];
+ int32_t frameIndex = renderer->tpagToSpriteFrameIndex[tpagIndex];
+ if (spriteIndex >= 0 && frameIndex >= 0) {
+ N3DSRenderer_tryLoadDirectSpriteImage(renderer, spriteIndex, frameIndex, &directImage);
+ }
+ }
+ if (directImage == NULL && renderer->tpagToBackgroundIndex != NULL &&
+ tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) {
+ int32_t backgroundIndex = renderer->tpagToBackgroundIndex[tpagIndex];
+ if (backgroundIndex >= 0) {
+ N3DSRenderer_tryLoadDirectBackgroundImage(renderer, backgroundIndex, &directImage);
+ }
+ }
+ if (directImage != NULL && directImage->subtex != NULL) {
+ Tex3DS_SubTexture subtex = *directImage->subtex;
+ float baseLeft = subtex.left;
+ float baseRight = subtex.right;
+ float baseTop = subtex.top;
+ float baseBottom = subtex.bottom;
+ float baseWidth = (float) directImage->subtex->width;
+ float baseHeight = (float) directImage->subtex->height;
+ if (baseWidth <= 0.0f || baseHeight <= 0.0f) return;
+
+ subtex.width = (uint16_t) srcW;
+ subtex.height = (uint16_t) srcH;
+ subtex.left = baseLeft + (baseRight - baseLeft) * ((float) srcOffX / baseWidth);
+ subtex.right = baseLeft + (baseRight - baseLeft) * (((float) srcOffX + (float) srcW) / baseWidth);
+ subtex.top = baseTop + (baseBottom - baseTop) * ((float) srcOffY / baseHeight);
+ subtex.bottom = baseTop + (baseBottom - baseTop) * (((float) srcOffY + (float) srcH) / baseHeight);
+
+ C2D_Image image = {
+ .tex = directImage->tex,
+ .subtex = &subtex,
+ };
+ N3DSRenderer_drawImage(base, &image, x, y, (float) srcW * xscale, (float) srcH * yscale, pivotX - x, pivotY - y, angleDeg, color, alpha);
+ return;
+ }
+ if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return;
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ int32_t reqLeft = srcOffX;
+ int32_t reqTop = srcOffY;
+ int32_t reqRight = srcOffX + srcW;
+ int32_t reqBottom = srcOffY + srcH;
+
+ repeat(item->fragmentCount, fragIndex) {
+ N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart + fragIndex];
+ int32_t fragLeft = fragment->sourceX;
+ int32_t fragTop = fragment->sourceY;
+ int32_t fragRight = fragLeft + fragment->width;
+ int32_t fragBottom = fragTop + fragment->height;
+ int32_t clipLeft = reqLeft > fragLeft ? reqLeft : fragLeft;
+ int32_t clipTop = reqTop > fragTop ? reqTop : fragTop;
+ int32_t clipRight = reqRight < fragRight ? reqRight : fragRight;
+ int32_t clipBottom = reqBottom < fragBottom ? reqBottom : fragBottom;
+ if (clipLeft >= clipRight || clipTop >= clipBottom) continue;
+
+ if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) continue;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) continue;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId];
+ if (!page->ready) continue;
+ renderer->frameFragmentDraws++;
+
+ uint16_t px = (uint16_t) (fragment->x + (uint16_t) (clipLeft - fragLeft));
+ uint16_t py = (uint16_t) (fragment->y + (uint16_t) (clipTop - fragTop));
+ int32_t clipW = clipRight - clipLeft;
+ int32_t clipH = clipBottom - clipTop;
+ Tex3DS_SubTexture subtex;
+ C2D_Image image = {
+ .tex = &page->texture,
+ .subtex = &subtex,
+ };
+ N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) clipW, (uint16_t) clipH);
+ float drawX = x + (float) (clipLeft - reqLeft) * xscale;
+ float drawY = y + (float) (clipTop - reqTop) * yscale;
+ float localPivotX = pivotX - drawX;
+ float localPivotY = pivotY - drawY;
+ N3DSRenderer_drawImage(base, &image, drawX, drawY, (float) clipW * xscale, (float) clipH * yscale, localPivotX, localPivotY, angleDeg, color, alpha);
+ }
+ return;
+ }
+
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return;
+ N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex];
+ if ((uint32_t) item->atlasId >= renderer->atlasPageCount) return;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, item->atlasId)) return;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[item->atlasId];
+ if (!page->ready) return;
+
+ Tex3DS_SubTexture subtex;
+ C2D_Image image = {
+ .tex = &page->texture,
+ .subtex = &subtex,
+ };
+ uint16_t px = (uint16_t) (item->x + srcOffX);
+ uint16_t py = (uint16_t) (item->y + srcOffY);
+ N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) srcW, (uint16_t) srcH);
+ N3DSRenderer_drawImage(base, &image, x, y, (float) srcW * xscale, (float) srcH * yscale, pivotX - x, pivotY - y, angleDeg, color, alpha);
+}
+
+static bool N3DSRenderer_tryDrawTileRect(
+ Renderer* base,
+ int32_t tpagIndex,
+ int32_t srcX,
+ int32_t srcY,
+ int32_t srcW,
+ int32_t srcH,
+ float drawX,
+ float drawY,
+ float xscale,
+ float yscale,
+ uint32_t color,
+ float alpha
+) {
+ if (srcW <= 0 || srcH <= 0) return false;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) {
+ N3DSRenderer_drawSpritePart(base, tpagIndex, srcX, srcY, srcW, srcH, drawX, drawY, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha);
+ return true;
+ }
+
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false;
+ N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex];
+ if (item->fragmentCount == 0) return false;
+
+ int32_t reqLeft = srcX;
+ int32_t reqTop = srcY;
+ int32_t reqRight = srcX + srcW;
+ int32_t reqBottom = srcY + srcH;
+ bool drewAnything = false;
+
+ if (item->fragmentCount == 1) {
+ N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart];
+ if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) return false;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId];
+ if (!page->ready) return false;
+
+ int32_t fragLeft = fragment->sourceX;
+ int32_t fragTop = fragment->sourceY;
+ int32_t fragRight = fragLeft + fragment->width;
+ int32_t fragBottom = fragTop + fragment->height;
+ int32_t clipLeft = reqLeft > fragLeft ? reqLeft : fragLeft;
+ int32_t clipTop = reqTop > fragTop ? reqTop : fragTop;
+ int32_t clipRight = reqRight < fragRight ? reqRight : fragRight;
+ int32_t clipBottom = reqBottom < fragBottom ? reqBottom : fragBottom;
+ if (clipLeft >= clipRight || clipTop >= clipBottom) return false;
+
+ uint16_t px = (uint16_t) (fragment->x + (uint16_t) (clipLeft - fragLeft));
+ uint16_t py = (uint16_t) (fragment->y + (uint16_t) (clipTop - fragTop));
+ int32_t clipW = clipRight - clipLeft;
+ int32_t clipH = clipBottom - clipTop;
+ Tex3DS_SubTexture subtex;
+ C2D_Image image = {
+ .tex = &page->texture,
+ .subtex = &subtex,
+ };
+ N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) clipW, (uint16_t) clipH);
+ renderer->frameFragmentDraws++;
+ N3DSRenderer_drawImageFast(
+ base,
+ &image,
+ drawX + (float) (clipLeft - reqLeft) * xscale,
+ drawY + (float) (clipTop - reqTop) * yscale,
+ xscale,
+ yscale,
+ color,
+ alpha
+ );
+ return true;
+ }
+
+ repeat(item->fragmentCount, fragIndex) {
+ N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart + fragIndex];
+ int32_t fragLeft = fragment->sourceX;
+ int32_t fragTop = fragment->sourceY;
+ int32_t fragRight = fragLeft + fragment->width;
+ int32_t fragBottom = fragTop + fragment->height;
+ int32_t clipLeft = reqLeft > fragLeft ? reqLeft : fragLeft;
+ int32_t clipTop = reqTop > fragTop ? reqTop : fragTop;
+ int32_t clipRight = reqRight < fragRight ? reqRight : fragRight;
+ int32_t clipBottom = reqBottom < fragBottom ? reqBottom : fragBottom;
+ if (clipLeft >= clipRight || clipTop >= clipBottom) continue;
+
+ if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) continue;
+ if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) continue;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId];
+ if (!page->ready) continue;
+
+ uint16_t px = (uint16_t) (fragment->x + (uint16_t) (clipLeft - fragLeft));
+ uint16_t py = (uint16_t) (fragment->y + (uint16_t) (clipTop - fragTop));
+ int32_t clipW = clipRight - clipLeft;
+ int32_t clipH = clipBottom - clipTop;
+ Tex3DS_SubTexture subtex;
+ C2D_Image image = {
+ .tex = &page->texture,
+ .subtex = &subtex,
+ };
+ N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) clipW, (uint16_t) clipH);
+ renderer->frameFragmentDraws++;
+ N3DSRenderer_drawImageFast(
+ base,
+ &image,
+ drawX + (float) (clipLeft - reqLeft) * xscale,
+ drawY + (float) (clipTop - reqTop) * yscale,
+ xscale,
+ yscale,
+ color,
+ alpha
+ );
+ drewAnything = true;
+ }
+
+ return drewAnything;
+}
+
+static bool N3DSRenderer_drawPackedTileEntry(
+ Renderer* base,
+ N3DSRenderer* renderer,
+ const N3DSTileAtlasEntry* tileEntry,
+ float drawX,
+ float drawY,
+ float xscale,
+ float yscale,
+ uint32_t color,
+ float alpha
+) {
+ if (tileEntry == NULL) return false;
+
+ if (tileEntry->fragmentCount == 0) {
+ if (tileEntry->width == 0 ||
+ tileEntry->height == 0 ||
+ tileEntry->atlasId >= renderer->atlasPageCount ||
+ !N3DSRenderer_ensurePageLoaded(renderer, tileEntry->atlasId)) {
+ return false;
+ }
+
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[tileEntry->atlasId];
+ if (!page->ready) return false;
+
+ Tex3DS_SubTexture subtex;
+ C2D_Image image = {
+ .tex = &page->texture,
+ .subtex = &subtex,
+ };
+ N3DSRenderer_fillSubTexture(&subtex, page, tileEntry->x, tileEntry->y, tileEntry->width, tileEntry->height);
+ N3DSRenderer_drawImageFast(base, &image, drawX, drawY, xscale, yscale, color, alpha);
+ return true;
+ }
+
+ bool drewAnything = false;
+ repeat(tileEntry->fragmentCount, fragIndex) {
+ uint32_t fragmentIndex = tileEntry->fragmentStart + (uint32_t) fragIndex;
+ if (fragmentIndex >= renderer->atlasFragmentCount) break;
+ N3DSAtlasFragment* fragment = &renderer->atlasFragments[fragmentIndex];
+ if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) continue;
+ N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId];
+ if (!page->ready) continue;
+
+ Tex3DS_SubTexture subtex;
+ C2D_Image image = {
+ .tex = &page->texture,
+ .subtex = &subtex,
+ };
+ N3DSRenderer_fillSubTexture(&subtex, page, fragment->x, fragment->y, fragment->width, fragment->height);
+ renderer->frameFragmentDraws++;
+ N3DSRenderer_drawImageFast(
+ base,
+ &image,
+ drawX + (float) fragment->sourceX * xscale,
+ drawY + (float) fragment->sourceY * yscale,
+ xscale,
+ yscale,
+ color,
+ alpha
+ );
+ drewAnything = true;
+ }
+
+ return drewAnything;
+}
+
+static void N3DSRenderer_drawTile(Renderer* base, RoomTile* tile, float offsetX, float offsetY) {
+ if (base == NULL || tile == NULL) return;
+ int32_t srcW = (int32_t) tile->width;
+ int32_t srcH = (int32_t) tile->height;
+ if (srcW <= 0 || srcH <= 0) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ uint32_t bgr = tile->color & 0x00FFFFFFu;
+ uint8_t alphaByte = (uint8_t) ((tile->color >> 24) & 0xFFu);
+ float alpha = (alphaByte == 0) ? 1.0f : (float) alphaByte / 255.0f;
+ float drawX = (float) tile->x + offsetX;
+ float drawY = (float) tile->y + offsetY;
+ if (!Renderer_isFiniteFloat(drawX) || !Renderer_isFiniteFloat(drawY) ||
+ !Renderer_isFiniteFloat(tile->scaleX) || !Renderer_isFiniteFloat(tile->scaleY) ||
+ tile->scaleX == 0.0f || tile->scaleY == 0.0f) {
+ return;
+ }
+
+ N3DSTileAtlasEntry* tileEntry = N3DSRenderer_findTileEntry(renderer, tile);
+ if (tileEntry != NULL &&
+ N3DSRenderer_drawPackedTileEntry(base, renderer, tileEntry, drawX, drawY, tile->scaleX, tile->scaleY, bgr, alpha)) {
+ return;
+ }
+
+ int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(base->dataWin, tile);
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= base->dataWin->tpag.count) return;
+ TexturePageItem* tpag = &base->dataWin->tpag.items[tpagIndex];
+
+ if (N3DSRenderer_tryDrawTileRect(
+ base,
+ tpagIndex,
+ tile->sourceX,
+ tile->sourceY,
+ srcW,
+ srcH,
+ drawX,
+ drawY,
+ tile->scaleX,
+ tile->scaleY,
+ bgr,
+ alpha
+ )) {
+ return;
+ }
+
+ int32_t fallbackSrcX = tile->sourceX - tpag->targetX;
+ int32_t fallbackSrcY = tile->sourceY - tpag->targetY;
+ (void) N3DSRenderer_tryDrawTileRect(
+ base,
+ tpagIndex,
+ fallbackSrcX,
+ fallbackSrcY,
+ srcW,
+ srcH,
+ drawX,
+ drawY,
+ tile->scaleX,
+ tile->scaleY,
+ bgr,
+ alpha
+ );
+}
+
+static void N3DSRenderer_drawTiled(Renderer* base, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha) {
+ if (!Renderer_isFiniteFloat(originX) || !Renderer_isFiniteFloat(originY) ||
+ !Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) ||
+ !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) ||
+ !Renderer_isFiniteFloat(roomW) || !Renderer_isFiniteFloat(roomH) ||
+ !Renderer_isFiniteFloat(alpha) || xscale == 0.0f || yscale == 0.0f) {
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (tpagIndex < 0 || (uint32_t) tpagIndex >= base->dataWin->tpag.count) return;
+ TexturePageItem* tpag = &base->dataWin->tpag.items[tpagIndex];
+ if (tpag->boundingWidth == 0 || tpag->boundingHeight == 0) return;
+
+ int32_t cropX = 0;
+ int32_t cropY = 0;
+ int32_t cropW = (int32_t) tpag->sourceWidth;
+ int32_t cropH = (int32_t) tpag->sourceHeight;
+ if (!N3DSRenderer_getSourceCropRect(renderer, tpagIndex, &cropX, &cropY, &cropW, &cropH)) {
+ return;
+ }
+ if (cropW <= 0 || cropH <= 0) return;
+
+ C2D_Image* directImage = NULL;
+ if (renderer->tpagToSpriteIndex != NULL && renderer->tpagToSpriteFrameIndex != NULL &&
+ tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) {
+ int32_t spriteIndex = renderer->tpagToSpriteIndex[tpagIndex];
+ int32_t frameIndex = renderer->tpagToSpriteFrameIndex[tpagIndex];
+ if (spriteIndex >= 0 && frameIndex >= 0) {
+ N3DSRenderer_tryLoadDirectSpriteImage(renderer, spriteIndex, frameIndex, &directImage);
+ }
+ }
+ if (directImage == NULL &&
+ renderer->tpagToBackgroundIndex != NULL &&
+ tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) {
+ int32_t backgroundIndex = renderer->tpagToBackgroundIndex[tpagIndex];
+ if (backgroundIndex >= 0) {
+ N3DSRenderer_tryLoadDirectBackgroundImage(renderer, backgroundIndex, &directImage);
+ }
+ }
+ if (directImage != NULL && directImage->subtex != NULL) {
+ cropX = 0;
+ cropY = 0;
+ cropW = (int32_t) directImage->subtex->width;
+ cropH = (int32_t) directImage->subtex->height;
+ }
+
+ float axScale = fabsf(xscale);
+ float ayScale = fabsf(yscale);
+ float tileW = (float) cropW * axScale;
+ float tileH = (float) cropH * ayScale;
+ if (tileW <= 0.0f || tileH <= 0.0f) return;
+
+ float startX;
+ float endX;
+ float startY;
+ float endY;
+ if (tileX) {
+ startX = fmodf(x - originX * axScale, tileW);
+ if (startX > 0.0f) startX -= tileW;
+ endX = roomW;
+ } else {
+ startX = x - originX * axScale;
+ endX = startX + tileW;
+ }
+ if (tileY) {
+ startY = fmodf(y - originY * ayScale, tileH);
+ if (startY > 0.0f) startY -= tileH;
+ endY = roomH;
+ } else {
+ startY = y - originY * ayScale;
+ endY = startY + tileH;
+ }
+
+ float dxLocalX0 = (float) cropX * xscale + originX * (axScale - xscale);
+ float dyLocalY0 = (float) cropY * yscale + originY * (ayScale - yscale);
+ float tileGameW = (float) cropW * xscale;
+ float tileGameH = (float) cropH * yscale;
+
+ int32_t tilesX = tileX ? ((int32_t) ((endX - startX) / tileW) + 1) : 1;
+ int32_t tilesY = tileY ? ((int32_t) ((endY - startY) / tileH) + 1) : 1;
+ if (tilesX <= 0 || tilesY <= 0) return;
+
+ float viewRight = (renderer->viewScaleX != 0.0f) ? ((float) renderer->viewX + ((float) N3DS_TOP_WIDTH / fabsf(renderer->viewScaleX))) : (float) renderer->viewX;
+ float viewBottom = (renderer->viewScaleY != 0.0f) ? ((float) renderer->viewY + ((float) N3DS_TOP_HEIGHT / fabsf(renderer->viewScaleY))) : (float) renderer->viewY;
+ int32_t startTileX = 0;
+ int32_t endTileX = tilesX;
+ int32_t startTileY = 0;
+ int32_t endTileY = tilesY;
+
+ if (tileX) {
+ float firstVisibleLeft = startX + dxLocalX0;
+ float minTileX = floorf((((float) renderer->viewX) - (firstVisibleLeft + tileGameW)) / tileW) + 1.0f;
+ float maxTileX = ceilf((viewRight - firstVisibleLeft) / tileW);
+ startTileX = (int32_t) minTileX;
+ endTileX = (int32_t) maxTileX;
+ if (startTileX < 0) startTileX = 0;
+ if (endTileX > tilesX) endTileX = tilesX;
+ }
+
+ if (tileY) {
+ float firstVisibleTop = startY + dyLocalY0;
+ float minTileY = floorf((((float) renderer->viewY) - (firstVisibleTop + tileGameH)) / tileH) + 1.0f;
+ float maxTileY = ceilf((viewBottom - firstVisibleTop) / tileH);
+ startTileY = (int32_t) minTileY;
+ endTileY = (int32_t) maxTileY;
+ if (startTileY < 0) startTileY = 0;
+ if (endTileY > tilesY) endTileY = tilesY;
+ }
+
+ if (startTileX >= endTileX || startTileY >= endTileY) return;
+
+ for (int32_t iy = startTileY; iy < endTileY; iy++) {
+ float dy = startY + (float) iy * tileH;
+ if (dy >= endY) break;
+
+ float localTop = dy + dyLocalY0;
+ float localBottom = localTop + tileGameH;
+ float localMinY = localTop < localBottom ? localTop : localBottom;
+ float localMaxY = localTop > localBottom ? localTop : localBottom;
+ float visTop = localMinY > (float) renderer->viewY ? localMinY : (float) renderer->viewY;
+ float visBottom = localMaxY < viewBottom ? localMaxY : viewBottom;
+ if (visTop >= visBottom) continue;
+
+ for (int32_t ix = startTileX; ix < endTileX; ix++) {
+ float dx = startX + (float) ix * tileW;
+ if (dx >= endX) break;
+
+ float localLeft = dx + dxLocalX0;
+ float localRight = localLeft + tileGameW;
+ float localMinX = localLeft < localRight ? localLeft : localRight;
+ float localMaxX = localLeft > localRight ? localLeft : localRight;
+ float visLeft = localMinX > (float) renderer->viewX ? localMinX : (float) renderer->viewX;
+ float visRight = localMaxX < viewRight ? localMaxX : viewRight;
+ if (visLeft >= visRight) continue;
+
+ float drawX = dx + dxLocalX0;
+ float drawY = dy + dyLocalY0;
+ if (directImage != NULL) {
+ renderer->frameDirectSpriteHits++;
+ if (fabsf(xscale - 1.0f) < 0.001f && fabsf(yscale - 1.0f) < 0.001f) {
+ N3DSRenderer_drawImageFast(base, directImage, drawX, drawY, 1.0f, 1.0f, color, alpha);
+ } else {
+ N3DSRenderer_drawImage(
+ base,
+ directImage,
+ drawX,
+ drawY,
+ (float) cropW * xscale,
+ (float) cropH * yscale,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ color,
+ alpha
+ );
+ }
+ } else if (!N3DSRenderer_tryDrawTileRect(base, tpagIndex, cropX, cropY, cropW, cropH, drawX, drawY, xscale, yscale, color, alpha)) {
+ N3DSRenderer_drawSpritePart(base, tpagIndex, cropX, cropY, cropW, cropH, drawX, drawY, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha);
+ }
+ }
+ }
+}
+
+static void N3DSRenderer_drawTiledPart(Renderer* base, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha) {
+ if (srcW <= 0 || srcH <= 0 || dstW <= 0.0f || dstH <= 0.0f) return;
+ if (!Renderer_isFiniteFloat(dstX) || !Renderer_isFiniteFloat(dstY) ||
+ !Renderer_isFiniteFloat(dstW) || !Renderer_isFiniteFloat(dstH) ||
+ !Renderer_isFiniteFloat(alpha)) {
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ int32_t cropX = 0;
+ int32_t cropY = 0;
+ int32_t cropW = 0;
+ int32_t cropH = 0;
+ if (!N3DSRenderer_getSourceCropRect(renderer, tpagIndex, &cropX, &cropY, &cropW, &cropH)) return;
+
+ int32_t tilesY = (int32_t) (dstH / (float) srcH) + 2;
+ int32_t tilesX = (int32_t) (dstW / (float) srcW) + 2;
+
+ repeat(tilesY, iy) {
+ float rowDstY = dstY + (float) iy * (float) srcH;
+ if (rowDstY >= dstY + dstH) break;
+ int32_t rowSrcH = srcH;
+ if (rowDstY + (float) rowSrcH > dstY + dstH) {
+ rowSrcH = (int32_t) ((dstY + dstH) - rowDstY);
+ }
+ if (rowSrcH <= 0) continue;
+
+ int32_t intY1 = srcY > cropY ? srcY : cropY;
+ int32_t intY2 = (srcY + rowSrcH) < (cropY + cropH) ? (srcY + rowSrcH) : (cropY + cropH);
+ if (intY1 >= intY2) continue;
+
+ float clipOffY = (float) (intY1 - srcY);
+ int32_t visH = intY2 - intY1;
+
+ repeat(tilesX, ix) {
+ float colDstX = dstX + (float) ix * (float) srcW;
+ if (colDstX >= dstX + dstW) break;
+ int32_t colSrcW = srcW;
+ if (colDstX + (float) colSrcW > dstX + dstW) {
+ colSrcW = (int32_t) ((dstX + dstW) - colDstX);
+ }
+ if (colSrcW <= 0) continue;
+
+ int32_t intX1 = srcX > cropX ? srcX : cropX;
+ int32_t intX2 = (srcX + colSrcW) < (cropX + cropW) ? (srcX + colSrcW) : (cropX + cropW);
+ if (intX1 >= intX2) continue;
+
+ float clipOffX = (float) (intX1 - srcX);
+ int32_t visW = intX2 - intX1;
+ float drawX = colDstX + clipOffX;
+ float drawY = rowDstY + clipOffY;
+
+ if (!N3DSRenderer_tryDrawTileRect(base, tpagIndex, intX1, intY1, visW, visH, drawX, drawY, 1.0f, 1.0f, color, alpha)) {
+ N3DSRenderer_drawSpritePart(base, tpagIndex, intX1, intY1, visW, visH, drawX, drawY, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, color, alpha);
+ }
+ }
+ }
+}
+
+static void N3DSRenderer_drawSpritePos(Renderer* base, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) {
+ if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) ||
+ !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) ||
+ !Renderer_isFiniteFloat(x3) || !Renderer_isFiniteFloat(y3) ||
+ !Renderer_isFiniteFloat(x4) || !Renderer_isFiniteFloat(y4) ||
+ !Renderer_isFiniteFloat(alpha)) {
+ return;
+ }
+
+ bool axisAligned =
+ fabsf(y1 - y2) < 0.01f &&
+ fabsf(x2 - x3) < 0.01f &&
+ fabsf(y3 - y4) < 0.01f &&
+ fabsf(x4 - x1) < 0.01f;
+ if (!axisAligned) return;
+
+ N3DSRenderer_drawSpritePart(
+ base,
+ tpagIndex,
+ 0,
+ 0,
+ (int32_t) lroundf(fabsf(x2 - x1)),
+ (int32_t) lroundf(fabsf(y4 - y1)),
+ x1,
+ y1,
+ 1.0f,
+ 1.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0xFFFFFF,
+ alpha
+ );
+}
+
+static void N3DSRenderer_drawRectangle(Renderer* base, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) {
+ if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) ||
+ !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) ||
+ !Renderer_isFiniteFloat(alpha)) {
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ float sx1 = N3DSRenderer_transformScreenX(renderer, x1);
+ float sy1 = N3DSRenderer_transformScreenY(renderer, y1);
+ float sx2 = N3DSRenderer_transformScreenX(renderer, x2);
+ float sy2 = N3DSRenderer_transformScreenY(renderer, y2);
+ float sx = sx1 < sx2 ? sx1 : sx2;
+ float sy = sy1 < sy2 ? sy1 : sy2;
+ float sw = fabsf(sx2 - sx1);
+ float sh = fabsf(sy2 - sy1);
+ u32 rgba = N3DSRenderer_makeColor(color, alpha);
+
+ if (outline) {
+ C2D_DrawLine(sx, sy, rgba, sx + sw, sy, rgba, 1.0f, 0.5f);
+ C2D_DrawLine(sx + sw, sy, rgba, sx + sw, sy + sh, rgba, 1.0f, 0.5f);
+ C2D_DrawLine(sx + sw, sy + sh, rgba, sx, sy + sh, rgba, 1.0f, 0.5f);
+ C2D_DrawLine(sx, sy + sh, rgba, sx, sy, rgba, 1.0f, 0.5f);
+ N3DSRenderer_noteC2DDraws(renderer, 4u);
+ } else {
+ C2D_DrawRectSolid(sx, sy, 0.5f, sw, sh, rgba);
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+ }
+}
+
+static void N3DSRenderer_drawLine(Renderer* base, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha) {
+ if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) ||
+ !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) ||
+ !Renderer_isFiniteFloat(width) || !Renderer_isFiniteFloat(alpha)) {
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ float sx1 = N3DSRenderer_transformScreenX(renderer, x1);
+ float sy1 = N3DSRenderer_transformScreenY(renderer, y1);
+ float sx2 = N3DSRenderer_transformScreenX(renderer, x2);
+ float sy2 = N3DSRenderer_transformScreenY(renderer, y2);
+ if (!Renderer_isFiniteFloat(sx1) || !Renderer_isFiniteFloat(sy1) ||
+ !Renderer_isFiniteFloat(sx2) || !Renderer_isFiniteFloat(sy2)) {
+ return;
+ }
+
+ width = fabsf(width);
+ if (width < 1.0f) width = 1.0f;
+
+ float dx = sx2 - sx1;
+ float dy = sy2 - sy1;
+ if ((dx * dx + dy * dy) <= 0.0001f) {
+ C2D_DrawRectSolid(sx1, sy1, 0.5f, width, width, N3DSRenderer_makeColor(color, alpha));
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+ return;
+ }
+
+ u32 rgba = N3DSRenderer_makeColor(color, alpha);
+ C2D_DrawLine(
+ sx1,
+ sy1,
+ rgba,
+ sx2,
+ sy2,
+ rgba,
+ width,
+ 0.5f
+ );
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+}
+
+static void N3DSRenderer_drawTriangle(Renderer* base, float x1, float y1, float x2, float y2, float x3, float y3, bool outline) {
+ if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) ||
+ !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) ||
+ !Renderer_isFiniteFloat(x3) || !Renderer_isFiniteFloat(y3)) {
+ return;
+ }
+
+ if (outline) {
+ N3DSRenderer_drawLine(base, x1, y1, x2, y2, 1.0f, base->drawColor, base->drawAlpha);
+ N3DSRenderer_drawLine(base, x2, y2, x3, y3, 1.0f, base->drawColor, base->drawAlpha);
+ N3DSRenderer_drawLine(base, x3, y3, x1, y1, 1.0f, base->drawColor, base->drawAlpha);
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ float sx1 = N3DSRenderer_transformScreenX(renderer, x1);
+ float sy1 = N3DSRenderer_transformScreenY(renderer, y1);
+ float sx2 = N3DSRenderer_transformScreenX(renderer, x2);
+ float sy2 = N3DSRenderer_transformScreenY(renderer, y2);
+ float sx3 = N3DSRenderer_transformScreenX(renderer, x3);
+ float sy3 = N3DSRenderer_transformScreenY(renderer, y3);
+ if (!Renderer_isFiniteFloat(sx1) || !Renderer_isFiniteFloat(sy1) ||
+ !Renderer_isFiniteFloat(sx2) || !Renderer_isFiniteFloat(sy2) ||
+ !Renderer_isFiniteFloat(sx3) || !Renderer_isFiniteFloat(sy3)) {
+ return;
+ }
+
+ float minX = sx1 < sx2 ? sx1 : sx2;
+ if (sx3 < minX) minX = sx3;
+ float maxX = sx1 > sx2 ? sx1 : sx2;
+ if (sx3 > maxX) maxX = sx3;
+ float minY = sy1 < sy2 ? sy1 : sy2;
+ if (sy3 < minY) minY = sy3;
+ float maxY = sy1 > sy2 ? sy1 : sy2;
+ if (sy3 > maxY) maxY = sy3;
+ if (N3DSRenderer_isScreenRectOffscreen(renderer, minX, minY, maxX - minX, maxY - minY)) {
+ return;
+ }
+
+ // Tiny circles and snapped coordinates can collapse adjacent fan triangles to zero area.
+ // Citro2D appears to dislike those degenerate cases, so skip them before issuing the draw.
+ float doubledArea =
+ ((sx2 - sx1) * (sy3 - sy1)) -
+ ((sy2 - sy1) * (sx3 - sx1));
+ if (fabsf(doubledArea) < 0.01f) {
+ return;
+ }
+
+ u32 rgba = N3DSRenderer_makeColor(base->drawColor, base->drawAlpha);
+ C2D_DrawTriangle(
+ sx1,
+ sy1,
+ rgba,
+ sx2,
+ sy2,
+ rgba,
+ sx3,
+ sy3,
+ rgba,
+ 0.5f
+ );
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+}
+
+static void N3DSRenderer_drawLineColor(Renderer* base, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha) {
+ if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) ||
+ !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) ||
+ !Renderer_isFiniteFloat(width) || !Renderer_isFiniteFloat(alpha)) {
+ return;
+ }
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ float sx1 = N3DSRenderer_transformScreenX(renderer, x1);
+ float sy1 = N3DSRenderer_transformScreenY(renderer, y1);
+ float sx2 = N3DSRenderer_transformScreenX(renderer, x2);
+ float sy2 = N3DSRenderer_transformScreenY(renderer, y2);
+ if (!Renderer_isFiniteFloat(sx1) || !Renderer_isFiniteFloat(sy1) ||
+ !Renderer_isFiniteFloat(sx2) || !Renderer_isFiniteFloat(sy2)) {
+ return;
+ }
+
+ width = fabsf(width);
+ if (width < 1.0f) width = 1.0f;
+
+ float dx = sx2 - sx1;
+ float dy = sy2 - sy1;
+ if ((dx * dx + dy * dy) <= 0.0001f) {
+ C2D_DrawRectSolid(sx1, sy1, 0.5f, width, width, N3DSRenderer_makeColor(color1, alpha));
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+ return;
+ }
+
+ C2D_DrawLine(
+ sx1,
+ sy1,
+ N3DSRenderer_makeColor(color1, alpha),
+ sx2,
+ sy2,
+ N3DSRenderer_makeColor(color2, alpha),
+ width,
+ 0.5f
+ );
+ N3DSRenderer_noteC2DDraws(renderer, 1u);
+}
+
+static void N3DSRenderer_drawTextCommon(Renderer* base, const char* text, float x, float y, float xscale, float yscale, float angleDeg, bool gradient, int32_t c1, int32_t c2, int32_t c3, int32_t c4, float alpha) {
+ DataWin* dw = base->dataWin;
+ if (base->drawFont < 0 || (uint32_t) base->drawFont >= dw->font.count) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ bool savedTextLinearFilterActive = renderer->textLinearFilterActive;
+ renderer->textLinearFilterActive = renderer->topScreenGui2xActive;
+ u64 textStartTick = svcGetSystemTick();
+ Font* font = &dw->font.fonts[base->drawFont];
+ float effectiveXScale = xscale * font->scaleX;
+ float effectiveYScale = yscale * font->scaleY;
+ if (!font->isSpriteFont && font->tpagIndex >= 0) {
+ N3DSRenderer_traceTPAGUsage(renderer, N3DS_TRACE_KIND_FONT, font->tpagIndex);
+ }
+ int32_t len = (int32_t) strlen(text);
+ const N3DSAtlasFragment* fontFragment = NULL;
+ N3DSLoadedAtlasPage* fontPage = NULL;
+ C2D_Image directFontImage;
+ Tex3DS_SubTexture directFontBaseSubtex;
+ bool useDirectFontAsset =
+ !font->isSpriteFont &&
+ N3DSRenderer_tryResolveDirectFontImage(renderer, base->drawFont, &directFontImage, &directFontBaseSubtex);
+ bool useSingleFragmentFontFastPath =
+ !useDirectFontAsset &&
+ !font->isSpriteFont &&
+ N3DSRenderer_tryResolveSingleFragmentFontPage(renderer, font->tpagIndex, &fontFragment, &fontPage);
+ if (useDirectFontAsset || useSingleFragmentFontFastPath) {
+ if (N3DSRenderer_tryDrawSingleGlyphTextFast(
+ base,
+ renderer,
+ font,
+ text,
+ len,
+ x,
+ y,
+ effectiveXScale,
+ effectiveYScale,
+ angleDeg,
+ gradient,
+ c1,
+ alpha,
+ useDirectFontAsset,
+ useDirectFontAsset ? &directFontImage : NULL,
+ useDirectFontAsset ? &directFontBaseSubtex : NULL,
+ fontFragment,
+ fontPage)) {
+ (void) c2;
+ (void) c3;
+ (void) c4;
+ renderer->frameTextTenthsMs += (uint32_t) lround(N3DSRenderer_ticksToMs(svcGetSystemTick() - textStartTick) * 10.0);
+ renderer->textLinearFilterActive = savedTextLinearFilterActive;
+ return;
+ }
+
+ const N3DSCachedTextLayout* layout = N3DSRenderer_getCachedTextLayout(renderer, font, base->drawFont, text);
+ repeat(layout->glyphCount, i) {
+ const N3DSCachedTextGlyph* cachedGlyph = &layout->glyphs[i];
+ Tex3DS_SubTexture subtex;
+ C2D_Image image;
+ if (useDirectFontAsset) {
+ subtex.width = cachedGlyph->sourceWidth;
+ subtex.height = cachedGlyph->sourceHeight;
+ float baseLeft = directFontBaseSubtex.left;
+ float baseRight = directFontBaseSubtex.right;
+ float baseTop = directFontBaseSubtex.top;
+ float baseBottom = directFontBaseSubtex.bottom;
+ float baseWidth = (float) directFontBaseSubtex.width;
+ float baseHeight = (float) directFontBaseSubtex.height;
+ subtex.left = baseLeft + (baseRight - baseLeft) * ((float) cachedGlyph->sourceX / baseWidth);
+ subtex.right = baseLeft + (baseRight - baseLeft) * (((float) cachedGlyph->sourceX + (float) cachedGlyph->sourceWidth) / baseWidth);
+ subtex.top = baseTop + (baseBottom - baseTop) * ((float) cachedGlyph->sourceY / baseHeight);
+ subtex.bottom = baseTop + (baseBottom - baseTop) * (((float) cachedGlyph->sourceY + (float) cachedGlyph->sourceHeight) / baseHeight);
+ image.tex = directFontImage.tex;
+ image.subtex = &subtex;
+ } else {
+ image.tex = &fontPage->texture;
+ image.subtex = &subtex;
+ uint16_t px = (uint16_t) (fontFragment->x + cachedGlyph->sourceX);
+ uint16_t py = (uint16_t) (fontFragment->y + cachedGlyph->sourceY);
+ N3DSRenderer_fillSubTexture(&subtex, fontPage, px, py, cachedGlyph->sourceWidth, cachedGlyph->sourceHeight);
+ }
+ float drawX = x + cachedGlyph->localX * effectiveXScale;
+ float drawY = y + cachedGlyph->localY * effectiveYScale;
+ renderer->frameTextGlyphDraws++;
+ if (fabsf(angleDeg) < 0.001f) {
+ N3DSRenderer_drawImageFast(base, &image, drawX, drawY, effectiveXScale, effectiveYScale, gradient ? (uint32_t) c1 : base->drawColor, alpha);
+ } else {
+ N3DSRenderer_drawImage(base, &image, drawX, drawY, (float) cachedGlyph->sourceWidth * effectiveXScale, (float) cachedGlyph->sourceHeight * effectiveYScale, 0.0f, 0.0f, angleDeg, gradient ? (uint32_t) c1 : base->drawColor, alpha);
+ }
+ }
+ (void) c2;
+ (void) c3;
+ (void) c4;
+ renderer->frameTextTenthsMs += (uint32_t) lround(N3DSRenderer_ticksToMs(svcGetSystemTick() - textStartTick) * 10.0);
+ renderer->textLinearFilterActive = savedTextLinearFilterActive;
+ return;
+ }
+ int32_t lineCount = TextUtils_countLines(text, len);
+ float totalHeight = (float) lineCount * TextUtils_lineStride(font) * effectiveYScale;
+ float valignOffset = 0.0f;
+ if (base->drawValign == 1) valignOffset = -totalHeight * 0.5f;
+ else if (base->drawValign == 2) valignOffset = -totalHeight;
+
+ float cursorY = y + valignOffset - (float) font->ascenderOffset * effectiveYScale;
+ int32_t lineStart = 0;
+
+ while (lineStart <= len) {
+ int32_t lineEnd = lineStart;
+ while (lineEnd < len && !TextUtils_isNewlineChar(text[lineEnd])) lineEnd++;
+ float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineEnd - lineStart) * effectiveXScale;
+ float cursorX = x;
+ if (base->drawHalign == 1) cursorX -= lineWidth * 0.5f;
+ else if (base->drawHalign == 2) cursorX -= lineWidth;
+
+ int32_t pos = lineStart;
+ while (pos < lineEnd) {
+ uint16_t ch = TextUtils_decodeUtf8(text, lineEnd, &pos);
+ FontGlyph* glyph = TextUtils_findGlyph(font, ch);
+ if (glyph == NULL) continue;
+
+ uint32_t glyphColor = gradient ? (uint32_t) c1 : base->drawColor;
+ if (font->isSpriteFont) {
+ int32_t glyphIndex = (int32_t) (glyph - font->glyphs);
+ Sprite* sprite = &dw->sprt.sprites[font->spriteIndex];
+ if (glyphIndex >= 0 && glyphIndex < (int32_t) sprite->textureCount) {
+ int32_t glyphTpagIndex = sprite->tpagIndices[glyphIndex];
+ if (glyphTpagIndex >= 0 && (uint32_t) glyphTpagIndex < dw->tpag.count) {
+ TexturePageItem* glyphTpag = &dw->tpag.items[glyphTpagIndex];
+ renderer->frameTextGlyphDraws++;
+ N3DSRenderer_drawSprite(
+ base,
+ glyphTpagIndex,
+ cursorX + (float) glyph->offset * effectiveXScale,
+ cursorY,
+ (float) glyphTpag->targetX,
+ (float) sprite->originY,
+ effectiveXScale,
+ effectiveYScale,
+ angleDeg,
+ glyphColor,
+ alpha
+ );
+ }
+ }
+ } else {
+ renderer->frameTextGlyphDraws++;
+ N3DSRenderer_drawSpritePart(
+ base,
+ font->tpagIndex,
+ glyph->sourceX,
+ glyph->sourceY,
+ glyph->sourceWidth,
+ glyph->sourceHeight,
+ cursorX + (float) glyph->offset * effectiveXScale,
+ cursorY,
+ effectiveXScale,
+ effectiveYScale,
+ angleDeg,
+ 0.0f,
+ 0.0f,
+ glyphColor,
+ alpha
+ );
+ }
+
+ if (pos < lineEnd) {
+ int32_t previewPos = pos;
+ uint16_t nextCh = TextUtils_decodeUtf8(text, lineEnd, &previewPos);
+ cursorX += ((float) glyph->shift + TextUtils_getKerningOffset(glyph, nextCh)) * effectiveXScale;
+ } else {
+ cursorX += (float) glyph->shift * effectiveXScale;
+ }
+ }
+
+ if (lineEnd >= len) break;
+ lineStart = TextUtils_skipNewline(text, lineEnd, len);
+ cursorY += TextUtils_lineStride(font) * effectiveYScale;
+ }
+
+ (void) c2;
+ (void) c3;
+ (void) c4;
+ renderer->frameTextTenthsMs += (uint32_t) lround(N3DSRenderer_ticksToMs(svcGetSystemTick() - textStartTick) * 10.0);
+ renderer->textLinearFilterActive = savedTextLinearFilterActive;
+}
+
+static void N3DSRenderer_drawText(Renderer* base, const char* text, float x, float y, float xscale, float yscale, float angleDeg) {
+ N3DSRenderer_drawTextCommon(base, text, x, y, xscale, yscale, angleDeg, false, 0, 0, 0, 0, base->drawAlpha);
+}
+
+static void N3DSRenderer_drawTextColor(Renderer* base, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t c1, int32_t c2, int32_t c3, int32_t c4, float alpha) {
+ N3DSRenderer_drawTextCommon(base, text, x, y, xscale, yscale, angleDeg, true, c1, c2, c3, c4, alpha);
+}
+
+static void N3DSRenderer_flush(Renderer* base) {
+ if (base == NULL) return;
+ N3DSRenderer_flushC2DQueue((N3DSRenderer*) base);
+}
+
+static void N3DSRenderer_clearScreen(Renderer* base, uint32_t color, float alpha) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->clearColor = color;
+ renderer->clearAlpha = alpha;
+ C2D_TargetClear(renderer->topTarget, N3DSRenderer_makeColor(color, alpha));
+}
+
+static int32_t N3DSRenderer_createSpriteFromSurface(Renderer* base, int32_t surfaceID, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig) {
+ if (base == NULL || base->dataWin == NULL || surfaceID != -1 || w <= 0 || h <= 0) return -1;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (renderer->activeSceneTarget != N3DS_SCENE_TARGET_TOP) return -1;
+
+ float screenRectX = 0.0f;
+ float screenRectY = 0.0f;
+ float screenRectW = 0.0f;
+ float screenRectH = 0.0f;
+ N3DSRenderer_transformScreenRect(renderer, (float) x, (float) y, (float) w, (float) h, &screenRectX, &screenRectY, &screenRectW, &screenRectH);
+
+ int32_t screenLeft = (int32_t) floorf(screenRectX);
+ int32_t screenTop = (int32_t) floorf(screenRectY);
+ int32_t screenRight = (int32_t) ceilf(screenRectX + screenRectW);
+ int32_t screenBottom = (int32_t) ceilf(screenRectY + screenRectH);
+ if (screenLeft < 0) screenLeft = 0;
+ if (screenTop < 0) screenTop = 0;
+ if (screenRight > N3DS_TOP_WIDTH) screenRight = N3DS_TOP_WIDTH;
+ if (screenBottom > N3DS_TOP_HEIGHT) screenBottom = N3DS_TOP_HEIGHT;
+ if (screenLeft >= screenRight || screenTop >= screenBottom) return -1;
+
+ uint16_t captureWidth = (uint16_t) (screenRight - screenLeft);
+ uint16_t captureHeight = (uint16_t) (screenBottom - screenTop);
+ size_t pixelCount = (size_t) captureWidth * (size_t) captureHeight;
+ uint16_t* pixels = (uint16_t*) linearAlloc(pixelCount * sizeof(uint16_t));
+ if (pixels == NULL) return -1;
+
+ N3DSRenderer_flushC2DQueue(renderer);
+ C2D_Flush();
+ uint8_t* framebuffer = gfxGetFramebuffer(GFX_TOP, GFX_LEFT, NULL, NULL);
+ if (framebuffer == NULL) {
+ linearFree(pixels);
+ return -1;
+ }
+
+ uint16_t transparentKey = 0;
+ bool haveTransparentKey = false;
+ repeat(captureHeight, row) {
+ int32_t screenY = screenTop + (int32_t) row;
+ repeat(captureWidth, col) {
+ int32_t screenX = screenLeft + (int32_t) col;
+ size_t fbOffset = ((size_t) screenX * (size_t) N3DS_TOP_HEIGHT + (size_t) ((N3DS_TOP_HEIGHT - 1) - screenY)) * 3u;
+ uint8_t r = framebuffer[fbOffset + 0u];
+ uint8_t g = framebuffer[fbOffset + 1u];
+ uint8_t b = framebuffer[fbOffset + 2u];
+ uint16_t rgba5551 = (uint16_t) ((((uint16_t) r >> 3) << 11) | (((uint16_t) g >> 3) << 6) | (((uint16_t) b >> 3) << 1) | 1u);
+ if (!haveTransparentKey) {
+ transparentKey = rgba5551;
+ haveTransparentKey = true;
+ }
+ if (removeback && rgba5551 == transparentKey) {
+ rgba5551 &= (uint16_t) ~1u;
+ }
+ pixels[(size_t) row * (size_t) captureWidth + (size_t) col] = rgba5551;
+ }
+ }
+
+ int32_t spriteIndex = (int32_t) DataWin_allocSpriteSlot(base->dataWin, base->dataWin->sprt.parsedCount);
+ int32_t tpagIndex = N3DSRenderer_allocDynamicCaptureTPAG(renderer);
+ if (spriteIndex < 0 || tpagIndex < 0) {
+ linearFree(pixels);
+ return -1;
+ }
+
+ N3DSDynamicCaptureTPAG* capture = &renderer->dynamicCaptureTPAGs[(uint32_t) tpagIndex - renderer->baseTPAGCount];
+ if (!C3D_TexInit(&capture->texture, captureWidth, captureHeight, GPU_RGBA5551)) {
+ linearFree(pixels);
+ return -1;
+ }
+
+ C3D_TexSetFilter(&capture->texture, smooth ? GPU_LINEAR : GPU_NEAREST, smooth ? GPU_LINEAR : GPU_NEAREST);
+ C3D_TexSetWrap(&capture->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE);
+ C3D_TexUpload(&capture->texture, pixels);
+ C3D_TexFlush(&capture->texture);
+ linearFree(pixels);
+
+ capture->used = true;
+ capture->ownerSpriteIndex = spriteIndex;
+ capture->logicalWidth = (uint16_t) w;
+ capture->logicalHeight = (uint16_t) h;
+ capture->subtex.width = captureWidth;
+ capture->subtex.height = captureHeight;
+ capture->subtex.left = 0.0f;
+ capture->subtex.top = 1.0f;
+ capture->subtex.right = 1.0f;
+ capture->subtex.bottom = 0.0f;
+ capture->image.tex = &capture->texture;
+ capture->image.subtex = &capture->subtex;
+
+ Sprite* sprite = &base->dataWin->sprt.sprites[spriteIndex];
+ const char* preservedName = sprite->name;
+ TexturePageItem* tpag = &base->dataWin->tpag.items[tpagIndex];
+ memset(sprite, 0, sizeof(*sprite));
+ sprite->name = preservedName;
+ sprite->width = (uint32_t) w;
+ sprite->height = (uint32_t) h;
+ sprite->transparent = removeback;
+ sprite->smooth = smooth;
+ sprite->originX = xorig;
+ sprite->originY = yorig;
+ sprite->textureCount = 1u;
+ sprite->tpagIndices = safeMalloc(sizeof(int32_t));
+ sprite->tpagIndices[0] = tpagIndex;
+
+ memset(tpag, 0, sizeof(*tpag));
+ tpag->sourceWidth = (uint16_t) w;
+ tpag->sourceHeight = (uint16_t) h;
+ tpag->targetWidth = (uint16_t) w;
+ tpag->targetHeight = (uint16_t) h;
+ tpag->boundingWidth = (uint16_t) w;
+ tpag->boundingHeight = (uint16_t) h;
+ tpag->texturePageId = -1;
+ return spriteIndex;
+}
+
+static void N3DSRenderer_deleteSprite(Renderer* base, int32_t spriteIndex) {
+ if (base == NULL || base->dataWin == NULL) return;
+ DataWin* dw = base->dataWin;
+ if (spriteIndex < 0 || (uint32_t) spriteIndex >= dw->sprt.count) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ Sprite* sprite = &dw->sprt.sprites[spriteIndex];
+ if (sprite->textureCount == 0) return;
+
+ bool releasedDynamicCapture = false;
+ repeat(renderer->dynamicCaptureTPAGCount, i) {
+ N3DSDynamicCaptureTPAG* capture = &renderer->dynamicCaptureTPAGs[i];
+ if (!capture->used || capture->ownerSpriteIndex != spriteIndex) continue;
+ N3DSRenderer_freeDynamicCaptureTPAG(renderer, capture);
+ TexturePageItem* tpag = &dw->tpag.items[renderer->baseTPAGCount + (uint32_t) i];
+ memset(tpag, 0, sizeof(*tpag));
+ tpag->texturePageId = -1;
+ releasedDynamicCapture = true;
+ }
+ if (!releasedDynamicCapture) return;
+
+ const char* preservedName = sprite->name;
+ free(sprite->tpagIndices);
+ sprite->tpagIndices = NULL;
+ repeat(sprite->maskCount, maskIndex) {
+ free(sprite->masks[maskIndex]);
+ }
+ free(sprite->masks);
+ memset(sprite, 0, sizeof(*sprite));
+ sprite->name = preservedName;
+}
+
+static void N3DSRenderer_gpuSetBlendMode(Renderer* base, int32_t mode) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->blendEnabled = true;
+ renderer->blendEquation = mode;
+ renderer->blendSrcFactor = (mode == bm_add) ? bm_src_alpha : bm_src_alpha;
+ renderer->blendDstFactor = (mode == bm_add) ? bm_one : bm_inv_src_alpha;
+ N3DSRenderer_applyBlendState(renderer);
+}
+
+static void N3DSRenderer_gpuSetBlendModeExt(Renderer* base, int32_t sfactor, int32_t dfactor) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->blendEnabled = true;
+ renderer->blendEquation = bm_normal;
+ renderer->blendSrcFactor = sfactor;
+ renderer->blendDstFactor = dfactor;
+ N3DSRenderer_applyBlendState(renderer);
+}
+
+static void N3DSRenderer_gpuSetBlendEnable(Renderer* base, bool enable) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->blendEnabled = enable;
+ N3DSRenderer_applyBlendState(renderer);
+}
+
+static void N3DSRenderer_gpuSetAlphaTestEnable(Renderer* base, bool enable) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->alphaTestEnabled = enable;
+ N3DSRenderer_applyAlphaState(renderer);
+}
+
+static void N3DSRenderer_gpuSetAlphaTestRef(Renderer* base, uint8_t ref) {
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ renderer->alphaTestRef = ref;
+ N3DSRenderer_applyAlphaState(renderer);
+}
+
+static void N3DSRenderer_gpuSetColorWriteEnable(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED bool red, MAYBE_UNUSED bool green, MAYBE_UNUSED bool blue, MAYBE_UNUSED bool alpha) {}
+static void N3DSRenderer_gpuSetFog(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED bool enable, MAYBE_UNUSED uint32_t color) {}
+
+static int32_t N3DSRenderer_createSurface(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t width, MAYBE_UNUSED int32_t height) { return -1; }
+static bool N3DSRenderer_surfaceExists(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return surfaceID == -1; }
+static bool N3DSRenderer_setSurfaceTarget(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return false; }
+static bool N3DSRenderer_resetSurfaceTarget(MAYBE_UNUSED Renderer* base) { return false; }
+static float N3DSRenderer_getSurfaceWidth(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return 0.0f; }
+static float N3DSRenderer_getSurfaceHeight(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return 0.0f; }
+static void N3DSRenderer_drawSurface(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED float x, MAYBE_UNUSED float y, MAYBE_UNUSED float xscale, MAYBE_UNUSED float yscale, MAYBE_UNUSED float angleDeg, MAYBE_UNUSED uint32_t color, MAYBE_UNUSED float alpha) {}
+static void N3DSRenderer_drawSurfacePart(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED int32_t x, MAYBE_UNUSED int32_t y, MAYBE_UNUSED int32_t left, MAYBE_UNUSED int32_t top, MAYBE_UNUSED int32_t width, MAYBE_UNUSED int32_t height, MAYBE_UNUSED float xscale, MAYBE_UNUSED float yscale, MAYBE_UNUSED uint32_t color, MAYBE_UNUSED float alpha) {}
+static void N3DSRenderer_drawSurfaceStretched(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED float x, MAYBE_UNUSED float y, MAYBE_UNUSED float width, MAYBE_UNUSED float height) {}
+static void N3DSRenderer_surfaceResize(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED int32_t width, MAYBE_UNUSED int32_t height) {}
+static void N3DSRenderer_surfaceFree(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) {}
+static void N3DSRenderer_surfaceCopy(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t DestSurfaceID, MAYBE_UNUSED int32_t DestX, MAYBE_UNUSED int32_t DestY, MAYBE_UNUSED int32_t SrcSurfaceID, MAYBE_UNUSED int32_t SrcX, MAYBE_UNUSED int32_t SrcY, MAYBE_UNUSED int32_t SrcW, MAYBE_UNUSED int32_t SrcH, MAYBE_UNUSED bool part) {}
+
+static RendererVtable N3DSRenderer_vtable = {
+ .init = N3DSRenderer_init,
+ .destroy = N3DSRenderer_destroy,
+ .beginFrame = N3DSRenderer_beginFrame,
+ .endFrame = N3DSRenderer_endFrame,
+ .beginView = N3DSRenderer_beginView,
+ .endView = N3DSRenderer_endView,
+ .beginGUI = N3DSRenderer_beginGUI,
+ .endGUI = N3DSRenderer_endGUI,
+ .drawSprite = N3DSRenderer_drawSprite,
+ .drawSpritePart = N3DSRenderer_drawSpritePart,
+ .drawSpritePos = N3DSRenderer_drawSpritePos,
+ .drawRectangle = N3DSRenderer_drawRectangle,
+ .drawLine = N3DSRenderer_drawLine,
+ .drawTriangle = N3DSRenderer_drawTriangle,
+ .drawLineColor = N3DSRenderer_drawLineColor,
+ .drawText = N3DSRenderer_drawText,
+ .drawTextColor = N3DSRenderer_drawTextColor,
+ .flush = N3DSRenderer_flush,
+ .clearScreen = N3DSRenderer_clearScreen,
+ .createSpriteFromSurface = N3DSRenderer_createSpriteFromSurface,
+ .deleteSprite = N3DSRenderer_deleteSprite,
+ .gpuSetBlendMode = N3DSRenderer_gpuSetBlendMode,
+ .gpuSetBlendModeExt = N3DSRenderer_gpuSetBlendModeExt,
+ .gpuSetBlendEnable = N3DSRenderer_gpuSetBlendEnable,
+ .gpuSetAlphaTestEnable = N3DSRenderer_gpuSetAlphaTestEnable,
+ .gpuSetAlphaTestRef = N3DSRenderer_gpuSetAlphaTestRef,
+ .gpuSetColorWriteEnable = N3DSRenderer_gpuSetColorWriteEnable,
+ .gpuSetFog = N3DSRenderer_gpuSetFog,
+ .drawTile = N3DSRenderer_drawTile,
+ .prewarmRoom = N3DSRenderer_prewarmRoom,
+ .drawTiled = N3DSRenderer_drawTiled,
+ .drawTiledPart = N3DSRenderer_drawTiledPart,
+ .createSurface = N3DSRenderer_createSurface,
+ .surfaceExists = N3DSRenderer_surfaceExists,
+ .setSurfaceTarget = N3DSRenderer_setSurfaceTarget,
+ .resetSurfaceTarget = N3DSRenderer_resetSurfaceTarget,
+ .getSurfaceWidth = N3DSRenderer_getSurfaceWidth,
+ .getSurfaceHeight = N3DSRenderer_getSurfaceHeight,
+ .drawSurface = N3DSRenderer_drawSurface,
+ .drawSurfacePart = N3DSRenderer_drawSurfacePart,
+ .drawSurfaceStretched = N3DSRenderer_drawSurfaceStretched,
+ .surfaceResize = N3DSRenderer_surfaceResize,
+ .surfaceFree = N3DSRenderer_surfaceFree,
+ .surfaceCopy = N3DSRenderer_surfaceCopy,
+};
+
+Renderer* N3DSRenderer_create(void) {
+ N3DSRenderer* renderer = safeCalloc(1, sizeof(N3DSRenderer));
+ renderer->base.vtable = &N3DSRenderer_vtable;
+ renderer->base.drawColor = 0xFFFFFF;
+ renderer->base.drawAlpha = 1.0f;
+ renderer->base.drawFont = -1;
+ renderer->base.drawHalign = 0;
+ renderer->base.drawValign = 0;
+ renderer->base.circlePrecision = 24;
+ renderer->resolvedAssetPathCache = NULL;
+ sh_new_strdup(renderer->resolvedAssetPathCache);
+ renderer->packedDirectAssetMap = NULL;
+ sh_new_strdup(renderer->packedDirectAssetMap);
+ return (Renderer*) renderer;
+}
+
+bool N3DSRenderer_isReady(Renderer* base) {
+ if (base == NULL) return false;
+ return ((N3DSRenderer*) base)->atlasLoaded;
+}
+
+const char* N3DSRenderer_getStartupError(Renderer* base) {
+ if (base == NULL) return "Renderer was not created";
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (renderer->startupError[0] == '\0') return NULL;
+ return renderer->startupError;
+}
+
+uint32_t N3DSRenderer_getResidentAtlasVRAMBytes(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->residentAtlasVRAMBytes;
+}
+
+uint32_t N3DSRenderer_getResidentAtlasVRAMLimitBytes(Renderer* base) {
+ if (base == NULL) return 0;
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ return renderer->residentAtlasVRAMLimitBytes;
+}
+
+uint32_t N3DSRenderer_getResidentAtlasPageCount(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->residentAtlasPageCount;
+}
+
+uint32_t N3DSRenderer_getResidentAtlasPageLimit(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->residentAtlasPageLimit;
+}
+
+uint32_t N3DSRenderer_getResidentDirectAssetVRAMBytes(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->residentDirectAssetVRAMBytes;
+}
+
+uint32_t N3DSRenderer_getResidentDirectAssetVRAMLimitBytes(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->residentDirectAssetVRAMLimitBytes;
+}
+
+uint32_t N3DSRenderer_getFrameFragmentDraws(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameFragmentDraws;
+}
+
+uint32_t N3DSRenderer_getFrameSpriteDrawCalls(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameSpriteDrawCalls;
+}
+
+uint32_t N3DSRenderer_getFrameSpritePartDrawCalls(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameSpritePartDrawCalls;
+}
+
+uint32_t N3DSRenderer_getFrameDirectSpriteHits(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameDirectSpriteHits;
+}
+
+uint32_t N3DSRenderer_getFrameDirectAssetLoads(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameDirectAssetLoads;
+}
+
+uint32_t N3DSRenderer_getFrameTextureSwitches(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameTextureSwitches;
+}
+
+uint32_t N3DSRenderer_getFrameTextGlyphDraws(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameTextGlyphDraws;
+}
+
+uint32_t N3DSRenderer_getFrameTextTenthsMs(Renderer* base) {
+ if (base == NULL) return 0;
+ return ((N3DSRenderer*) base)->frameTextTenthsMs;
+}
+
+int32_t N3DSRenderer_findTileEntryIndex(Renderer* base, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH) {
+ if (base == NULL) return -1;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ uint32_t entryIndex = 0;
+ if (N3DSRenderer_findTileEntryByKey(renderer, backgroundIndex, srcX, srcY, srcW, srcH, &entryIndex) == NULL) {
+ return -1;
+ }
+ return (int32_t) entryIndex;
+}
+
+bool N3DSRenderer_drawCachedTileEntry(Renderer* base, int32_t tileEntryIndex, float drawX, float drawY, float xscale, float yscale, uint32_t color, float alpha) {
+ if (base == NULL) return false;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (tileEntryIndex < 0 || (uint32_t) tileEntryIndex >= renderer->tileEntryCount) return false;
+ return N3DSRenderer_drawPackedTileEntry(base, renderer, &renderer->tileEntries[tileEntryIndex], drawX, drawY, xscale, yscale, color, alpha);
+}
+
+int32_t N3DSRenderer_createTileLayerChunkCache(Renderer* base, int32_t roomWidth, int32_t roomHeight, const TileLayerRenderCache* cache) {
+ if (base == NULL || cache == NULL || cache->rows == NULL || cache->cells == NULL) return -1;
+ if (roomWidth <= 0 || roomHeight <= 0 || cache->tileWidth == 0 || cache->tileHeight == 0) return -1;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ uint32_t chunkCols = ((uint32_t) roomWidth + (N3DS_TILE_LAYER_CHUNK_SIZE - 1u)) / N3DS_TILE_LAYER_CHUNK_SIZE;
+ uint32_t chunkRows = ((uint32_t) roomHeight + (N3DS_TILE_LAYER_CHUNK_SIZE - 1u)) / N3DS_TILE_LAYER_CHUNK_SIZE;
+ uint32_t chunkCount = chunkCols * chunkRows;
+ if (chunkCount == 0) return -1;
+
+ uint64_t estimatedBytes = (uint64_t) chunkCount * (uint64_t) N3DS_TILE_LAYER_CHUNK_TEXTURE_BYTES;
+ if (estimatedBytes > (uint64_t) N3DS_TILE_LAYER_CHUNK_VRAM_BUDGET ||
+ (uint64_t) renderer->tileLayerChunkVRAMBytes + estimatedBytes > (uint64_t) N3DS_TILE_LAYER_CHUNK_VRAM_BUDGET) {
+ return -1;
+ }
+
+ int32_t cacheIndex = -1;
+ size_t existingCount = arrlenu(renderer->tileLayerChunkCaches);
+ repeat(existingCount, i) {
+ if (!renderer->tileLayerChunkCaches[i].used) {
+ cacheIndex = (int32_t) i;
+ break;
+ }
+ }
+ if (cacheIndex < 0) {
+ N3DSTileLayerChunkCache newCache = {0};
+ arrput(renderer->tileLayerChunkCaches, newCache);
+ cacheIndex = (int32_t) arrlen(renderer->tileLayerChunkCaches) - 1;
+ }
+
+ N3DSTileLayerChunkCache* chunkCache = &renderer->tileLayerChunkCaches[cacheIndex];
+ N3DSRenderer_freeTileLayerChunkCache(renderer, chunkCache);
+ chunkCache->used = true;
+ chunkCache->chunkCount = chunkCount;
+ chunkCache->vramBytes = 0;
+ chunkCache->chunks = safeCalloc(chunkCount, sizeof(N3DSTileLayerChunk));
+
+ float savedFrameScaleX = renderer->frameScaleX;
+ float savedFrameScaleY = renderer->frameScaleY;
+ float savedFrameOffsetX = renderer->frameOffsetX;
+ float savedFrameOffsetY = renderer->frameOffsetY;
+ float savedPortOffsetX = renderer->portOffsetX;
+ float savedPortOffsetY = renderer->portOffsetY;
+ int32_t savedViewX = renderer->viewX;
+ int32_t savedViewY = renderer->viewY;
+ float savedViewScaleX = renderer->viewScaleX;
+ float savedViewScaleY = renderer->viewScaleY;
+ uint8_t savedSceneTarget = renderer->activeSceneTarget;
+
+ C2D_Flush();
+ renderer->pendingC2DDraws = 0;
+ renderer->lastDrawTexture = NULL;
+ renderer->frameScaleX = 1.0f;
+ renderer->frameScaleY = 1.0f;
+ renderer->frameOffsetX = 0.0f;
+ renderer->frameOffsetY = 0.0f;
+ renderer->portOffsetX = 0.0f;
+ renderer->portOffsetY = 0.0f;
+ renderer->viewX = 0;
+ renderer->viewY = 0;
+ renderer->viewScaleX = 1.0f;
+ renderer->viewScaleY = 1.0f;
+ N3DSRenderer_applyBlendState(renderer);
+ N3DSRenderer_applyAlphaState(renderer);
+
+ repeat(chunkRows, chunkRow) {
+ repeat(chunkCols, chunkCol) {
+ uint32_t chunkIndexU = (uint32_t) chunkRow * chunkCols + (uint32_t) chunkCol;
+ N3DSTileLayerChunk* chunk = &chunkCache->chunks[chunkIndexU];
+ chunk->x = (int32_t) chunkCol * (int32_t) N3DS_TILE_LAYER_CHUNK_SIZE;
+ chunk->y = (int32_t) chunkRow * (int32_t) N3DS_TILE_LAYER_CHUNK_SIZE;
+ chunk->width = (uint16_t) (((uint32_t) roomWidth - (uint32_t) chunk->x) > N3DS_TILE_LAYER_CHUNK_SIZE ? N3DS_TILE_LAYER_CHUNK_SIZE : ((uint32_t) roomWidth - (uint32_t) chunk->x));
+ chunk->height = (uint16_t) (((uint32_t) roomHeight - (uint32_t) chunk->y) > N3DS_TILE_LAYER_CHUNK_SIZE ? N3DS_TILE_LAYER_CHUNK_SIZE : ((uint32_t) roomHeight - (uint32_t) chunk->y));
+ if (chunk->width == 0 || chunk->height == 0) continue;
+
+ if (!C3D_TexInit(&chunk->texture, N3DS_TILE_LAYER_CHUNK_SIZE, N3DS_TILE_LAYER_CHUNK_SIZE, GPU_RGBA5551)) {
+ continue;
+ }
+ chunk->allocated = true;
+ C3D_TexSetFilter(&chunk->texture, GPU_NEAREST, GPU_NEAREST);
+ C3D_TexSetWrap(&chunk->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE);
+ chunk->target = C3D_RenderTargetCreateFromTex(&chunk->texture, GPU_TEXFACE_2D, 0, -1);
+ if (chunk->target == NULL) {
+ C3D_TexDelete(&chunk->texture);
+ memset(&chunk->texture, 0, sizeof(chunk->texture));
+ chunk->allocated = false;
+ continue;
+ }
+
+ chunk->subtex.width = chunk->width;
+ chunk->subtex.height = chunk->height;
+ chunk->subtex.left = 0.0f;
+ chunk->subtex.top = 1.0f;
+ chunk->subtex.right = (float) chunk->width / (float) N3DS_TILE_LAYER_CHUNK_SIZE;
+ chunk->subtex.bottom = 1.0f - ((float) chunk->height / (float) N3DS_TILE_LAYER_CHUNK_SIZE);
+ chunk->image.tex = &chunk->texture;
+ chunk->image.subtex = &chunk->subtex;
+
+ C2D_SceneBegin(chunk->target);
+ renderer->activeSceneTarget = N3DS_SCENE_TARGET_NONE;
+ C2D_TargetClear(chunk->target, C2D_Color32(0, 0, 0, 0));
+
+ int32_t startX = chunk->x / (int32_t) cache->tileWidth;
+ int32_t startY = chunk->y / (int32_t) cache->tileHeight;
+ int32_t endX = (int32_t) (((int64_t) chunk->x + (int64_t) chunk->width + (int64_t) cache->tileWidth - 1) / (int64_t) cache->tileWidth);
+ int32_t endY = (int32_t) (((int64_t) chunk->y + (int64_t) chunk->height + (int64_t) cache->tileHeight - 1) / (int64_t) cache->tileHeight);
+ if (startX < 0) startX = 0;
+ if (startY < 0) startY = 0;
+ if (endX > (int32_t) cache->tilesX) endX = (int32_t) cache->tilesX;
+ if (endY > (int32_t) cache->tilesY) endY = (int32_t) cache->tilesY;
+
+ bool drewAny = false;
+ for (int32_t ty = startY; ty < endY; ty++) {
+ const TileLayerCacheRow* rowCache = &cache->rows[ty];
+ if (rowCache->count == 0) continue;
+
+ const TileLayerCacheCell* rowCells = &cache->cells[rowCache->start];
+ uint32_t startCell = 0;
+ while (startCell < rowCache->count && (int32_t) rowCells[startCell].tileX < startX) startCell++;
+ for (uint32_t ci = startCell; ci < rowCache->count; ci++) {
+ const TileLayerCacheCell* cachedCell = &rowCells[ci];
+ if ((int32_t) cachedCell->tileX >= endX) break;
+
+ float xscale = cachedCell->mirror ? -1.0f : 1.0f;
+ float yscale = cachedCell->flip ? -1.0f : 1.0f;
+ float dstX = (float) ((int32_t) cachedCell->tileX * (int32_t) cache->tileWidth - chunk->x) + (cachedCell->mirror ? (float) cache->tileWidth : 0.0f);
+ float dstY = (float) (ty * (int32_t) cache->tileHeight - chunk->y) + (cachedCell->flip ? (float) cache->tileHeight : 0.0f);
+
+ bool drewCell = false;
+ if (cachedCell->n3dsTileEntryIndex >= 0 && (uint32_t) cachedCell->n3dsTileEntryIndex < renderer->tileEntryCount) {
+ drewCell = N3DSRenderer_drawPackedTileEntry(
+ base,
+ renderer,
+ &renderer->tileEntries[cachedCell->n3dsTileEntryIndex],
+ dstX,
+ dstY,
+ xscale,
+ yscale,
+ 0x00FFFFFFu,
+ 1.0f
+ );
+ }
+
+ if (!drewCell && cachedCell->tpagIndex >= 0) {
+ drewCell = N3DSRenderer_tryDrawTileRect(
+ base,
+ cachedCell->tpagIndex,
+ (int32_t) cachedCell->srcX,
+ (int32_t) cachedCell->srcY,
+ (int32_t) cache->tileWidth,
+ (int32_t) cache->tileHeight,
+ dstX,
+ dstY,
+ xscale,
+ yscale,
+ 0x00FFFFFFu,
+ 1.0f
+ );
+ }
+
+ if (drewCell) drewAny = true;
+ }
+ }
+
+ C2D_Flush();
+ renderer->pendingC2DDraws = 0;
+ renderer->lastDrawTexture = NULL;
+ if (!drewAny) {
+ if (chunk->target != NULL) {
+ C3D_RenderTargetDelete(chunk->target);
+ chunk->target = NULL;
+ }
+ C3D_TexDelete(&chunk->texture);
+ memset(&chunk->texture, 0, sizeof(chunk->texture));
+ chunk->allocated = false;
+ } else {
+ chunkCache->vramBytes += N3DS_TILE_LAYER_CHUNK_TEXTURE_BYTES;
+ }
+ }
+ }
+
+ renderer->tileLayerChunkVRAMBytes += chunkCache->vramBytes;
+
+ renderer->frameScaleX = savedFrameScaleX;
+ renderer->frameScaleY = savedFrameScaleY;
+ renderer->frameOffsetX = savedFrameOffsetX;
+ renderer->frameOffsetY = savedFrameOffsetY;
+ renderer->portOffsetX = savedPortOffsetX;
+ renderer->portOffsetY = savedPortOffsetY;
+ renderer->viewX = savedViewX;
+ renderer->viewY = savedViewY;
+ renderer->viewScaleX = savedViewScaleX;
+ renderer->viewScaleY = savedViewScaleY;
+ N3DSRenderer_applyBlendState(renderer);
+ N3DSRenderer_applyAlphaState(renderer);
+ if (savedSceneTarget == N3DS_SCENE_TARGET_BOTTOM) {
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_BOTTOM, true);
+ } else {
+ N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, true);
+ }
+
+ return cacheIndex;
+}
+
+bool N3DSRenderer_drawTileLayerChunkCache(Renderer* base, int32_t cacheId, float layerOffsetX, float layerOffsetY, float alpha) {
+ if (base == NULL) return false;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (cacheId < 0 || (size_t) cacheId >= arrlenu(renderer->tileLayerChunkCaches)) return false;
+ N3DSTileLayerChunkCache* cache = &renderer->tileLayerChunkCaches[cacheId];
+ if (!cache->used || cache->chunks == NULL) return false;
+
+ float viewRight = (renderer->viewScaleX != 0.0f) ? ((float) renderer->viewX + ((float) N3DS_TOP_WIDTH / fabsf(renderer->viewScaleX))) : (float) renderer->viewX;
+ float viewBottom = (renderer->viewScaleY != 0.0f) ? ((float) renderer->viewY + ((float) N3DS_TOP_HEIGHT / fabsf(renderer->viewScaleY))) : (float) renderer->viewY;
+ bool drewAny = false;
+
+ repeat(cache->chunkCount, i) {
+ N3DSTileLayerChunk* chunk = &cache->chunks[i];
+ if (!chunk->allocated || chunk->target == NULL) continue;
+
+ float localX = (float) chunk->x + layerOffsetX;
+ float localY = (float) chunk->y + layerOffsetY;
+ if (localX + (float) chunk->width <= (float) renderer->viewX ||
+ localY + (float) chunk->height <= (float) renderer->viewY ||
+ localX >= viewRight ||
+ localY >= viewBottom) {
+ continue;
+ }
+
+ renderer->frameSpriteDrawCalls++;
+ N3DSRenderer_drawImageFast(base, &chunk->image, localX, localY, 1.0f, 1.0f, 0x00FFFFFFu, alpha);
+ drewAny = true;
+ }
+
+ return drewAny;
+}
+
+void N3DSRenderer_destroyTileLayerChunkCache(Renderer* base, int32_t cacheId) {
+ if (base == NULL) return;
+
+ N3DSRenderer* renderer = (N3DSRenderer*) base;
+ if (cacheId < 0 || (size_t) cacheId >= arrlenu(renderer->tileLayerChunkCaches)) return;
+ N3DSRenderer_freeTileLayerChunkCache(renderer, &renderer->tileLayerChunkCaches[cacheId]);
+}
diff --git a/src/n3ds/n3ds_renderer.h b/src/n3ds/n3ds_renderer.h
new file mode 100644
index 00000000..aab13ff6
--- /dev/null
+++ b/src/n3ds/n3ds_renderer.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "../renderer.h"
+
+Renderer* N3DSRenderer_create(void);
+bool N3DSRenderer_isReady(Renderer* renderer);
+const char* N3DSRenderer_getStartupError(Renderer* renderer);
+void N3DSRenderer_beginOverlay(Renderer* renderer);
+void N3DSRenderer_beginBottomScreenGUIEx(Renderer* renderer, int32_t guiW, int32_t guiH, float scaleX, float scaleY, float offsetX, float offsetY);
+void N3DSRenderer_beginBottomScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH);
+void N3DSRenderer_endBottomScreenGUI(Renderer* renderer);
+void N3DSRenderer_beginBottomScreenGUI2x(Renderer* renderer, int32_t guiW, int32_t guiH);
+void N3DSRenderer_endBottomScreenGUI2x(Renderer* renderer);
+void N3DSRenderer_beginTopScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH);
+void N3DSRenderer_endTopScreenGUI(Renderer* renderer);
+void N3DSRenderer_beginTopScreenGUI2x(Renderer* renderer, int32_t guiW, int32_t guiH);
+void N3DSRenderer_endTopScreenGUI2x(Renderer* renderer);
+bool N3DSRenderer_isTopScreenGUIActive(Renderer* renderer);
+bool N3DSRenderer_isTopScreenBattleViewActive(Renderer* renderer);
+void N3DSRenderer_setTopScreenBattleViewActive(Renderer* renderer, bool active);
+uint32_t N3DSRenderer_getResidentAtlasVRAMBytes(Renderer* renderer);
+uint32_t N3DSRenderer_getResidentAtlasVRAMLimitBytes(Renderer* renderer);
+uint32_t N3DSRenderer_getResidentAtlasPageCount(Renderer* renderer);
+uint32_t N3DSRenderer_getResidentAtlasPageLimit(Renderer* renderer);
+uint32_t N3DSRenderer_getResidentDirectAssetVRAMBytes(Renderer* renderer);
+uint32_t N3DSRenderer_getResidentDirectAssetVRAMLimitBytes(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameFragmentDraws(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameSpriteDrawCalls(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameSpritePartDrawCalls(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameDirectSpriteHits(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameDirectAssetLoads(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameTextureSwitches(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameTextGlyphDraws(Renderer* renderer);
+uint32_t N3DSRenderer_getFrameTextTenthsMs(Renderer* renderer);
+int32_t N3DSRenderer_findTileEntryIndex(Renderer* renderer, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH);
+bool N3DSRenderer_drawCachedTileEntry(Renderer* renderer, int32_t tileEntryIndex, float drawX, float drawY, float xscale, float yscale, uint32_t color, float alpha);
diff --git a/src/ps2/stb_impl.c b/src/n3ds/stb_impl.c
similarity index 52%
rename from src/ps2/stb_impl.c
rename to src/n3ds/stb_impl.c
index 9df32fd7..61070306 100644
--- a/src/ps2/stb_impl.c
+++ b/src/n3ds/stb_impl.c
@@ -1,2 +1,2 @@
#define STB_DS_IMPLEMENTATION
-#include "stb_ds.h"
+#include
diff --git a/src/noop_audio_system.c b/src/noop_audio_system.c
index 27b62be8..05ae141a 100644
--- a/src/noop_audio_system.c
+++ b/src/noop_audio_system.c
@@ -96,6 +96,7 @@ static AudioSystemVtable noopVtable = {
.groupIsLoaded = noopGroupIsLoaded,
.createStream = noopCreateStream,
.destroyStream = noopDestroyStream,
+ .prewarmRoom = NULL,
};
NoopAudioSystem* NoopAudioSystem_create(void) {
diff --git a/src/noop_file_system.c b/src/noop_file_system.c
index fc335fd8..c4e9036c 100644
--- a/src/noop_file_system.c
+++ b/src/noop_file_system.c
@@ -1,146 +1,146 @@
-#include "noop_file_system.h"
-#include "utils.h"
-
-#include
-#include
-
-#include "stb_ds.h"
-
-// ===[ In-Memory File Storage ]===
-
-typedef struct {
- char* key; // file path
- char* value; // file contents
-} MemoryFileEntry;
-
-typedef struct {
- uint8_t* data;
- int32_t size;
-} MemoryBinaryData;
-
-typedef struct {
- char* key; // file path
- MemoryBinaryData value;
-} MemoryBinaryEntry;
-
-typedef struct {
- FileSystem base;
- MemoryFileEntry* files; // stb_ds string hashmap
- MemoryBinaryEntry* binaryFiles; // stb_ds string hashmap
-} NoopFileSystem;
-
-// ===[ Vtable Implementations ]===
-
-static char* noopResolvePath(MAYBE_UNUSED FileSystem* fs, MAYBE_UNUSED const char* relativePath) {
- return safeStrdup("./");
-}
-
-static bool noopFileExists(FileSystem* fs, const char* relativePath) {
- NoopFileSystem* nfs = (NoopFileSystem*) fs;
- return shgeti(nfs->files, relativePath) >= 0;
-}
-
-static char* noopReadFileText(FileSystem* fs, const char* relativePath) {
- NoopFileSystem* nfs = (NoopFileSystem*) fs;
- ptrdiff_t idx = shgeti(nfs->files, relativePath);
- if (0 > idx)
- return nullptr;
- return safeStrdup(nfs->files[idx].value);
-}
-
-static bool noopWriteFileText(FileSystem* fs, const char* relativePath, const char* contents) {
- NoopFileSystem* nfs = (NoopFileSystem*) fs;
-
- // If the key already exists, free the old value before overwriting
- ptrdiff_t idx = shgeti(nfs->files, relativePath);
- if (idx >= 0) {
- free(nfs->files[idx].value);
- nfs->files[idx].value = safeStrdup(contents);
- } else {
- shput(nfs->files, relativePath, safeStrdup(contents));
- }
-
- return true;
-}
-
-static bool noopDeleteFile(FileSystem* fs, const char* relativePath) {
- NoopFileSystem* nfs = (NoopFileSystem*) fs;
- ptrdiff_t idx = shgeti(nfs->files, relativePath);
- if (0 > idx)
- return false;
-
- free(nfs->files[idx].value);
- shdel(nfs->files, relativePath);
- return true;
-}
-
-static bool noopReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) {
- NoopFileSystem* nfs = (NoopFileSystem*) fs;
- ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath);
- if (0 > idx)
- return false;
-
- MemoryBinaryData* entry = &nfs->binaryFiles[idx].value;
- uint8_t* copy = safeMalloc((size_t) entry->size);
- memcpy(copy, entry->data, (size_t) entry->size);
- *outData = copy;
- *outSize = entry->size;
- return true;
-}
-
-static bool noopWriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) {
- NoopFileSystem* nfs = (NoopFileSystem*) fs;
-
- ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath);
- if (idx >= 0) {
- free(nfs->binaryFiles[idx].value.data);
- uint8_t* copy = safeMalloc((size_t) size);
- memcpy(copy, data, (size_t) size);
- nfs->binaryFiles[idx].value.data = copy;
- nfs->binaryFiles[idx].value.size = size;
- } else {
- uint8_t* copy = safeMalloc((size_t) size);
- memcpy(copy, data, (size_t) size);
- MemoryBinaryData binaryData = { .data = copy, .size = size };
- shput(nfs->binaryFiles, relativePath, binaryData);
- }
-
- return true;
-}
-
-// ===[ Vtable ]===
-
-static FileSystemVtable noopFileSystemVtable = {
- .resolvePath = noopResolvePath,
- .fileExists = noopFileExists,
- .readFileText = noopReadFileText,
- .writeFileText = noopWriteFileText,
- .deleteFile = noopDeleteFile,
- .readFileBinary = noopReadFileBinary,
- .writeFileBinary = noopWriteFileBinary,
-};
-
-// ===[ Lifecycle ]===
-
-FileSystem* NoopFileSystem_create(void) {
- NoopFileSystem* nfs = safeCalloc(1, sizeof(NoopFileSystem));
- nfs->base.vtable = &noopFileSystemVtable;
- nfs->files = nullptr;
- sh_new_strdup(nfs->files);
- nfs->binaryFiles = nullptr;
- sh_new_strdup(nfs->binaryFiles);
- return (FileSystem*) nfs;
-}
-
-void NoopFileSystem_destroy(FileSystem* fs) {
- NoopFileSystem* nfs = (NoopFileSystem*) fs;
- repeat(shlen(nfs->files), i) {
- free(nfs->files[i].value);
- }
- shfree(nfs->files);
- repeat(shlen(nfs->binaryFiles), i) {
- free(nfs->binaryFiles[i].value.data);
- }
- shfree(nfs->binaryFiles);
- free(nfs);
-}
+#include "noop_file_system.h"
+#include "utils.h"
+
+#include
+#include
+
+#include "stb_ds.h"
+
+// ===[ In-Memory File Storage ]===
+
+typedef struct {
+ char* key; // file path
+ char* value; // file contents
+} MemoryFileEntry;
+
+typedef struct {
+ uint8_t* data;
+ int32_t size;
+} MemoryBinaryData;
+
+typedef struct {
+ char* key; // file path
+ MemoryBinaryData value;
+} MemoryBinaryEntry;
+
+typedef struct {
+ FileSystem base;
+ MemoryFileEntry* files; // stb_ds string hashmap
+ MemoryBinaryEntry* binaryFiles; // stb_ds string hashmap
+} NoopFileSystem;
+
+// ===[ Vtable Implementations ]===
+
+static char* noopResolvePath(MAYBE_UNUSED FileSystem* fs, MAYBE_UNUSED const char* relativePath) {
+ return safeStrdup("./");
+}
+
+static bool noopFileExists(FileSystem* fs, const char* relativePath) {
+ NoopFileSystem* nfs = (NoopFileSystem*) fs;
+ return shgeti(nfs->files, relativePath) >= 0;
+}
+
+static char* noopReadFileText(FileSystem* fs, const char* relativePath) {
+ NoopFileSystem* nfs = (NoopFileSystem*) fs;
+ ptrdiff_t idx = shgeti(nfs->files, relativePath);
+ if (0 > idx)
+ return nullptr;
+ return safeStrdup(nfs->files[idx].value);
+}
+
+static bool noopWriteFileText(FileSystem* fs, const char* relativePath, const char* contents) {
+ NoopFileSystem* nfs = (NoopFileSystem*) fs;
+
+ // If the key already exists, free the old value before overwriting
+ ptrdiff_t idx = shgeti(nfs->files, relativePath);
+ if (idx >= 0) {
+ free(nfs->files[idx].value);
+ nfs->files[idx].value = safeStrdup(contents);
+ } else {
+ shput(nfs->files, relativePath, safeStrdup(contents));
+ }
+
+ return true;
+}
+
+static bool noopDeleteFile(FileSystem* fs, const char* relativePath) {
+ NoopFileSystem* nfs = (NoopFileSystem*) fs;
+ ptrdiff_t idx = shgeti(nfs->files, relativePath);
+ if (0 > idx)
+ return false;
+
+ free(nfs->files[idx].value);
+ shdel(nfs->files, relativePath);
+ return true;
+}
+
+static bool noopReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) {
+ NoopFileSystem* nfs = (NoopFileSystem*) fs;
+ ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath);
+ if (0 > idx)
+ return false;
+
+ MemoryBinaryData* entry = &nfs->binaryFiles[idx].value;
+ uint8_t* copy = safeMalloc((size_t) entry->size);
+ memcpy(copy, entry->data, (size_t) entry->size);
+ *outData = copy;
+ *outSize = entry->size;
+ return true;
+}
+
+static bool noopWriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) {
+ NoopFileSystem* nfs = (NoopFileSystem*) fs;
+
+ ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath);
+ if (idx >= 0) {
+ free(nfs->binaryFiles[idx].value.data);
+ uint8_t* copy = safeMalloc((size_t) size);
+ memcpy(copy, data, (size_t) size);
+ nfs->binaryFiles[idx].value.data = copy;
+ nfs->binaryFiles[idx].value.size = size;
+ } else {
+ uint8_t* copy = safeMalloc((size_t) size);
+ memcpy(copy, data, (size_t) size);
+ MemoryBinaryData binaryData = { .data = copy, .size = size };
+ shput(nfs->binaryFiles, relativePath, binaryData);
+ }
+
+ return true;
+}
+
+// ===[ Vtable ]===
+
+static FileSystemVtable noopFileSystemVtable = {
+ .resolvePath = noopResolvePath,
+ .fileExists = noopFileExists,
+ .readFileText = noopReadFileText,
+ .writeFileText = noopWriteFileText,
+ .deleteFile = noopDeleteFile,
+ .readFileBinary = noopReadFileBinary,
+ .writeFileBinary = noopWriteFileBinary,
+};
+
+// ===[ Lifecycle ]===
+
+FileSystem* NoopFileSystem_create(void) {
+ NoopFileSystem* nfs = safeCalloc(1, sizeof(NoopFileSystem));
+ nfs->base.vtable = &noopFileSystemVtable;
+ nfs->files = nullptr;
+ sh_new_strdup(nfs->files);
+ nfs->binaryFiles = nullptr;
+ sh_new_strdup(nfs->binaryFiles);
+ return (FileSystem*) nfs;
+}
+
+void NoopFileSystem_destroy(FileSystem* fs) {
+ NoopFileSystem* nfs = (NoopFileSystem*) fs;
+ repeat(shlen(nfs->files), i) {
+ free(nfs->files[i].value);
+ }
+ shfree(nfs->files);
+ repeat(shlen(nfs->binaryFiles), i) {
+ free(nfs->binaryFiles[i].value.data);
+ }
+ shfree(nfs->binaryFiles);
+ free(nfs);
+}
diff --git a/src/noop_file_system.h b/src/noop_file_system.h
index 9f3eb839..1ae29fa8 100644
--- a/src/noop_file_system.h
+++ b/src/noop_file_system.h
@@ -1,10 +1,10 @@
-#pragma once
-
-#include "common.h"
-#include "file_system.h"
-
-// Creates an in-memory FileSystem backed by a hashmap instead of real disk I/O
-// Files written via writeFileText are kept in memory and can be read back
-// Use this as a fallback while you don't have a proper file system implementation for your target!
-FileSystem* NoopFileSystem_create(void);
-void NoopFileSystem_destroy(FileSystem* fs);
+#pragma once
+
+#include "common.h"
+#include "file_system.h"
+
+// Creates an in-memory FileSystem backed by a hashmap instead of real disk I/O
+// Files written via writeFileText are kept in memory and can be read back
+// Use this as a fallback while you don't have a proper file system implementation for your target!
+FileSystem* NoopFileSystem_create(void);
+void NoopFileSystem_destroy(FileSystem* fs);
diff --git a/src/profiler.c b/src/profiler.c
index e98a80ff..da3134e0 100644
--- a/src/profiler.c
+++ b/src/profiler.c
@@ -1,177 +1,177 @@
-#include "profiler.h"
-
-#include
-#include
-#include
-
-#include "utils.h"
-#include "stb_ds.h"
-#include "string_builder.h"
-
-#if defined(PLATFORM_PS2)
-#include
-#elif defined(_WIN32)
-#include
-#else
-#include
-#endif
-
-static uint64_t nowNanos(void) {
-#if defined(PLATFORM_PS2)
- // kBUSCLK is bus clock ticks per second (~147 MHz).
- // Split to avoid u64 overflow in ticks * 1e9.
- uint64_t t = (uint64_t) GetTimerSystemTime();
- uint64_t clk = (uint64_t) kBUSCLK;
- uint64_t sec = t / clk;
- uint64_t rem = t % clk;
- return sec * 1000000000ull + (rem * 1000000000ull) / clk;
-#elif defined(_WIN32)
- static LARGE_INTEGER freq;
- static bool freqInitialized = false;
- if (!freqInitialized) {
- QueryPerformanceFrequency(&freq);
- freqInitialized = true;
- }
- LARGE_INTEGER now;
- QueryPerformanceCounter(&now);
- uint64_t t = (uint64_t) now.QuadPart;
- uint64_t f = (uint64_t) freq.QuadPart;
- uint64_t sec = t / f;
- uint64_t rem = t % f;
- return sec * 1000000000ull + (rem * 1000000000ull) / f;
-#else
- struct timespec ts;
- clock_gettime(CLOCK_MONOTONIC, &ts);
- return (uint64_t) ts.tv_sec * 1000000000ull + (uint64_t) ts.tv_nsec;
-#endif
-}
-
-Profiler* Profiler_create(void) {
- Profiler* p = safeMalloc(sizeof(Profiler));
- p->entries = nullptr;
- p->frameDepth = 0;
- p->instructionCount = 0;
- return p;
-}
-
-void Profiler_destroy(Profiler* p) {
- if (p == nullptr) return;
- shfree(p->entries);
- free(p);
-}
-
-void Profiler_setEnabled(Profiler** slot, bool enabled) {
- if (enabled) {
- if (*slot == nullptr) *slot = Profiler_create();
- } else {
- if (*slot != nullptr) {
- Profiler_destroy(*slot);
- *slot = nullptr;
- }
- }
-}
-
-void Profiler_enter(Profiler* p, const char* name) {
- if (p == nullptr) return;
- if (p->frameDepth >= PROFILER_MAX_DEPTH) return;
- ProfilerFrame* f = &p->frameStack[p->frameDepth];
- f->startNanos = nowNanos();
- f->childNanos = 0;
- f->startOps = p->instructionCount;
- f->childOps = 0;
- f->name = name != nullptr ? name : "";
- p->frameDepth++;
-}
-
-void Profiler_exit(Profiler* p) {
- if (p == nullptr) return;
- if (0 >= p->frameDepth) return;
- p->frameDepth--;
- ProfilerFrame* f = &p->frameStack[p->frameDepth];
- uint64_t elapsed = nowNanos() - f->startNanos;
- uint64_t selfNanos = elapsed > f->childNanos ? elapsed - f->childNanos : 0;
- uint64_t totalOps = p->instructionCount - f->startOps;
- uint64_t selfOps = totalOps > f->childOps ? totalOps - f->childOps : 0;
-
- ptrdiff_t i = shgeti(p->entries, f->name);
- if (0 > i) {
- ProfilerStats stats = { .nanos = selfNanos, .ops = selfOps };
- shput(p->entries, f->name, stats);
- } else {
- p->entries[i].value.nanos += selfNanos;
- p->entries[i].value.ops += selfOps;
- }
-
- if (p->frameDepth > 0) {
- p->frameStack[p->frameDepth - 1].childNanos += elapsed;
- p->frameStack[p->frameDepth - 1].childOps += totalOps;
- }
-}
-
-static int compareEntriesDesc(const void* a, const void* b) {
- uint64_t va = ((const ProfilerEntry*) a)->value.nanos;
- uint64_t vb = ((const ProfilerEntry*) b)->value.nanos;
- if (vb > va) return 1;
- if (va > vb) return -1;
- return 0;
-}
-
-// Sort entries into a caller-owned buffer. Returns entry count; 0 if nothing to report.
-// Also computes the grand total (across all entries, not just topN) in *outTotal.
-static size_t collectSorted(const Profiler* p, ProfilerEntry* outSorted, size_t outCap, ProfilerStats* outTotal) {
- size_t count = shlen(p->entries);
- if (count == 0) return 0;
- if (count > outCap) count = outCap;
- memcpy(outSorted, p->entries, count * sizeof(ProfilerEntry));
- qsort(outSorted, count, sizeof(ProfilerEntry), compareEntriesDesc);
-
- ProfilerStats total = { 0 };
- size_t fullCount = shlen(p->entries);
- repeat(fullCount, i) {
- total.nanos += p->entries[i].value.nanos;
- total.ops += p->entries[i].value.ops;
- }
- *outTotal = total;
- return count;
-}
-
-void Profiler_reset(Profiler* p) {
- if (p == nullptr) return;
- shfree(p->entries);
- p->entries = nullptr;
-}
-
-char* Profiler_createReport(const Profiler* p, int topN, int framesInWindow) {
- if (p == nullptr) return nullptr;
- size_t count = shlen(p->entries);
- if (count == 0) return nullptr;
- if (0 >= framesInWindow) framesInWindow = 1;
-
- ProfilerEntry* sorted = (ProfilerEntry*) malloc(count * sizeof(ProfilerEntry));
- if (sorted == nullptr) return nullptr;
- ProfilerStats total = { 0 };
- size_t sortedEntriesCount = collectSorted(p, sorted, count, &total);
-
- size_t limit = sortedEntriesCount;
- if (topN > 0 && (size_t) topN < limit)
- limit = (size_t) topN;
-
- StringBuilder stringBuilder = StringBuilder_create(64);
-
- double frames = (double) framesInWindow;
- double totalMs = total.nanos / 1000000.0;
- double totalOpsPerFrame = (double) total.ops / frames;
-
- StringBuilder_appendFormat(&stringBuilder, "GML Profiler (avg %d frames)\n", framesInWindow);
- repeat(limit, i) {
- double perFrameMs = ((double) sorted[i].value.nanos / (double) 1000000) / frames;
- double opsPerFrame = (double) sorted[i].value.ops / frames;
- double nsPerOp = sorted[i].value.ops > 0 ? (double) sorted[i].value.nanos / (double) sorted[i].value.ops : (double) 0;
- StringBuilder_appendFormat(&stringBuilder, "%.2fms %.0f ops (%.0f ns/op) %s\n", perFrameMs, opsPerFrame, nsPerOp, sorted[i].key);
- }
- StringBuilder_appendFormat(&stringBuilder, "total %.2fms/frame, %.0f ops/frame (%zu scripts)", totalMs / frames, totalOpsPerFrame, sortedEntriesCount);
- char* result = StringBuilder_toString(&stringBuilder);
- StringBuilder_free(&stringBuilder);
- free(sorted);
- return result;
-}
+#include "profiler.h"
+
+#include
+#include
+#include
+
+#include "utils.h"
+#include "stb_ds.h"
+#include "string_builder.h"
+
+#if defined(PLATFORM_PS2)
+#include
+#elif defined(_WIN32)
+#include
+#else
+#include
+#endif
+
+static uint64_t nowNanos(void) {
+#if defined(PLATFORM_PS2)
+ // kBUSCLK is bus clock ticks per second (~147 MHz).
+ // Split to avoid u64 overflow in ticks * 1e9.
+ uint64_t t = (uint64_t) GetTimerSystemTime();
+ uint64_t clk = (uint64_t) kBUSCLK;
+ uint64_t sec = t / clk;
+ uint64_t rem = t % clk;
+ return sec * 1000000000ull + (rem * 1000000000ull) / clk;
+#elif defined(_WIN32)
+ static LARGE_INTEGER freq;
+ static bool freqInitialized = false;
+ if (!freqInitialized) {
+ QueryPerformanceFrequency(&freq);
+ freqInitialized = true;
+ }
+ LARGE_INTEGER now;
+ QueryPerformanceCounter(&now);
+ uint64_t t = (uint64_t) now.QuadPart;
+ uint64_t f = (uint64_t) freq.QuadPart;
+ uint64_t sec = t / f;
+ uint64_t rem = t % f;
+ return sec * 1000000000ull + (rem * 1000000000ull) / f;
+#else
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint64_t) ts.tv_sec * 1000000000ull + (uint64_t) ts.tv_nsec;
+#endif
+}
+
+Profiler* Profiler_create(void) {
+ Profiler* p = safeMalloc(sizeof(Profiler));
+ p->entries = nullptr;
+ p->frameDepth = 0;
+ p->instructionCount = 0;
+ return p;
+}
+
+void Profiler_destroy(Profiler* p) {
+ if (p == nullptr) return;
+ shfree(p->entries);
+ free(p);
+}
+
+void Profiler_setEnabled(Profiler** slot, bool enabled) {
+ if (enabled) {
+ if (*slot == nullptr) *slot = Profiler_create();
+ } else {
+ if (*slot != nullptr) {
+ Profiler_destroy(*slot);
+ *slot = nullptr;
+ }
+ }
+}
+
+void Profiler_enter(Profiler* p, const char* name) {
+ if (p == nullptr) return;
+ if (p->frameDepth >= PROFILER_MAX_DEPTH) return;
+ ProfilerFrame* f = &p->frameStack[p->frameDepth];
+ f->startNanos = nowNanos();
+ f->childNanos = 0;
+ f->startOps = p->instructionCount;
+ f->childOps = 0;
+ f->name = name != nullptr ? name : "";
+ p->frameDepth++;
+}
+
+void Profiler_exit(Profiler* p) {
+ if (p == nullptr) return;
+ if (0 >= p->frameDepth) return;
+ p->frameDepth--;
+ ProfilerFrame* f = &p->frameStack[p->frameDepth];
+ uint64_t elapsed = nowNanos() - f->startNanos;
+ uint64_t selfNanos = elapsed > f->childNanos ? elapsed - f->childNanos : 0;
+ uint64_t totalOps = p->instructionCount - f->startOps;
+ uint64_t selfOps = totalOps > f->childOps ? totalOps - f->childOps : 0;
+
+ ptrdiff_t i = shgeti(p->entries, f->name);
+ if (0 > i) {
+ ProfilerStats stats = { .nanos = selfNanos, .ops = selfOps };
+ shput(p->entries, f->name, stats);
+ } else {
+ p->entries[i].value.nanos += selfNanos;
+ p->entries[i].value.ops += selfOps;
+ }
+
+ if (p->frameDepth > 0) {
+ p->frameStack[p->frameDepth - 1].childNanos += elapsed;
+ p->frameStack[p->frameDepth - 1].childOps += totalOps;
+ }
+}
+
+static int compareEntriesDesc(const void* a, const void* b) {
+ uint64_t va = ((const ProfilerEntry*) a)->value.nanos;
+ uint64_t vb = ((const ProfilerEntry*) b)->value.nanos;
+ if (vb > va) return 1;
+ if (va > vb) return -1;
+ return 0;
+}
+
+// Sort entries into a caller-owned buffer. Returns entry count; 0 if nothing to report.
+// Also computes the grand total (across all entries, not just topN) in *outTotal.
+static size_t collectSorted(const Profiler* p, ProfilerEntry* outSorted, size_t outCap, ProfilerStats* outTotal) {
+ size_t count = shlen(p->entries);
+ if (count == 0) return 0;
+ if (count > outCap) count = outCap;
+ memcpy(outSorted, p->entries, count * sizeof(ProfilerEntry));
+ qsort(outSorted, count, sizeof(ProfilerEntry), compareEntriesDesc);
+
+ ProfilerStats total = { 0 };
+ size_t fullCount = shlen(p->entries);
+ repeat(fullCount, i) {
+ total.nanos += p->entries[i].value.nanos;
+ total.ops += p->entries[i].value.ops;
+ }
+ *outTotal = total;
+ return count;
+}
+
+void Profiler_reset(Profiler* p) {
+ if (p == nullptr) return;
+ shfree(p->entries);
+ p->entries = nullptr;
+}
+
+char* Profiler_createReport(const Profiler* p, int topN, int framesInWindow) {
+ if (p == nullptr) return nullptr;
+ size_t count = shlen(p->entries);
+ if (count == 0) return nullptr;
+ if (0 >= framesInWindow) framesInWindow = 1;
+
+ ProfilerEntry* sorted = (ProfilerEntry*) malloc(count * sizeof(ProfilerEntry));
+ if (sorted == nullptr) return nullptr;
+ ProfilerStats total = { 0 };
+ size_t sortedEntriesCount = collectSorted(p, sorted, count, &total);
+
+ size_t limit = sortedEntriesCount;
+ if (topN > 0 && (size_t) topN < limit)
+ limit = (size_t) topN;
+
+ StringBuilder stringBuilder = StringBuilder_create(64);
+
+ double frames = (double) framesInWindow;
+ double totalMs = total.nanos / 1000000.0;
+ double totalOpsPerFrame = (double) total.ops / frames;
+
+ StringBuilder_appendFormat(&stringBuilder, "GML Profiler (avg %d frames)\n", framesInWindow);
+ repeat(limit, i) {
+ double perFrameMs = ((double) sorted[i].value.nanos / (double) 1000000) / frames;
+ double opsPerFrame = (double) sorted[i].value.ops / frames;
+ double nsPerOp = sorted[i].value.ops > 0 ? (double) sorted[i].value.nanos / (double) sorted[i].value.ops : (double) 0;
+ StringBuilder_appendFormat(&stringBuilder, "%.2fms %.0f ops (%.0f ns/op) %s\n", perFrameMs, opsPerFrame, nsPerOp, sorted[i].key);
+ }
+ StringBuilder_appendFormat(&stringBuilder, "total %.2fms/frame, %.0f ops/frame (%zu scripts)", totalMs / frames, totalOpsPerFrame, sortedEntriesCount);
+ char* result = StringBuilder_toString(&stringBuilder);
+ StringBuilder_free(&stringBuilder);
+ free(sorted);
+ return result;
+}
diff --git a/src/ps2/gs_renderer.c b/src/ps2/gs_renderer.c
deleted file mode 100644
index 1239e684..00000000
--- a/src/ps2/gs_renderer.c
+++ /dev/null
@@ -1,2175 +0,0 @@
-#include "gs_renderer.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "binary_reader.h"
-#include "binary_utils.h"
-#include "utils.h"
-#include "text_utils.h"
-#include "ps2_utils.h"
-#include "matrix_math.h"
-
-#ifdef ENABLE_PS2_RENDERER_LOGS
-#define rendererPrintf(...) fprintf(stderr, __VA_ARGS__)
-#else
-#define rendererPrintf(...) ((void) 0)
-#endif
-
-// ===[ Constants ]===
-#define ATLAS_WIDTH 512
-#define ATLAS_HEIGHT 512
-#define PS2_SCREEN_WIDTH 640.0f
-#define PS2_SCREEN_HEIGHT 448.0f
-#define TEX_HEADER_SIZE 128
-#define CLUT4_ENTRY_SIZE 64 // 16 colors * 4 bytes
-#define CLUT8_ENTRY_SIZE 1024 // 256 colors * 4 bytes
-
-// ===[ File Loading Helper ]===
-
-// Loads an entire file from host into a memalign'd buffer. Returns size via outSize.
-// Aborts on failure.
-static uint8_t* loadFileRaw(const char* path, uint32_t* outSize) {
- char* textureBinPath = PS2Utils_createDevicePath(path);
-
- FILE* f = fopen(textureBinPath, "rb");
- if (f == nullptr) {
- fprintf(stderr, "GsRenderer: Failed to open %s\n", path);
- abort();
- }
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- // 128-byte aligned for DMA transfers
- uint8_t* data = (uint8_t*) safeMemalign(128, (size_t) size);
-
- size_t read = fread(data, 1, (size_t) size, f);
- fclose(f);
-
- if (read != (size_t) size) {
- fprintf(stderr, "GsRenderer: Short read on %s (expected %ld, got %zu)\n", path, size, read);
- abort();
- }
-
- *outSize = (uint32_t) size;
- free(textureBinPath);
- return data;
-}
-
-// ===[ Atlas Loading ]===
-static void loadAtlas(GsRenderer* gs) {
- char* atlasBinPath = PS2Utils_createDevicePath("ATLAS.BIN");
- FILE* f = fopen(atlasBinPath, "rb");
- if (f == nullptr) {
- fprintf(stderr, "GsRenderer: Failed to open %s\n", atlasBinPath);
- abort();
- }
-
- fseek(f, 0, SEEK_END);
- size_t fileSize = (size_t) ftell(f);
- fseek(f, 0, SEEK_SET);
-
- BinaryReader reader = BinaryReader_create(f, fileSize);
-
- uint8_t version = BinaryReader_readUint8(&reader);
- if (version != 0) {
- fprintf(stderr, "GsRenderer: Unsupported ATLAS.BIN version %u\n", version);
- abort();
- }
-
- gs->atlasTPAGCount = BinaryReader_readUint16(&reader);
- gs->atlasTileCount = BinaryReader_readUint16(&reader);
- gs->atlasCount = BinaryReader_readUint16(&reader);
-
- // Parse atlas offset table
- gs->atlasOffsets = safeMalloc(gs->atlasCount * sizeof(uint32_t));
- repeat(gs->atlasCount, i) {
- gs->atlasOffsets[i] = BinaryReader_readUint32(&reader);
- }
-
- // Parse TPAG entries
- gs->atlasTPAGEntries = safeMalloc(gs->atlasTPAGCount * sizeof(AtlasTPAGEntry));
-
- repeat(gs->atlasTPAGCount, i) {
- AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[i];
- entry->atlasId = BinaryReader_readUint16(&reader);
- entry->atlasX = BinaryReader_readUint16(&reader);
- entry->atlasY = BinaryReader_readUint16(&reader);
- entry->width = BinaryReader_readUint16(&reader);
- entry->height = BinaryReader_readUint16(&reader);
- entry->cropX = BinaryReader_readUint16(&reader);
- entry->cropY = BinaryReader_readUint16(&reader);
- entry->cropW = BinaryReader_readUint16(&reader);
- entry->cropH = BinaryReader_readUint16(&reader);
- entry->clutIndex = BinaryReader_readUint16(&reader);
- entry->bpp = BinaryReader_readUint8(&reader);
- }
-
- // Parse tile entries
- gs->atlasTileEntries = safeMalloc(gs->atlasTileCount * sizeof(AtlasTileEntry));
-
- repeat(gs->atlasTileCount, i) {
- AtlasTileEntry* entry = &gs->atlasTileEntries[i];
- entry->bgDef = BinaryReader_readInt16(&reader);
- entry->srcX = BinaryReader_readUint16(&reader);
- entry->srcY = BinaryReader_readUint16(&reader);
- entry->srcW = BinaryReader_readUint16(&reader);
- entry->srcH = BinaryReader_readUint16(&reader);
- entry->atlasId = BinaryReader_readUint16(&reader);
- entry->atlasX = BinaryReader_readUint16(&reader);
- entry->atlasY = BinaryReader_readUint16(&reader);
- entry->width = BinaryReader_readUint16(&reader);
- entry->height = BinaryReader_readUint16(&reader);
- entry->cropX = BinaryReader_readUint16(&reader);
- entry->cropY = BinaryReader_readUint16(&reader);
- entry->cropW = BinaryReader_readUint16(&reader);
- entry->cropH = BinaryReader_readUint16(&reader);
- entry->clutIndex = BinaryReader_readUint16(&reader);
- entry->bpp = BinaryReader_readUint8(&reader);
- }
-
- fclose(f);
-
- // Build tile entry hashmap for O(1) lookup
- gs->tileEntryMap = nullptr;
- repeat(gs->atlasTileCount, i) {
- AtlasTileEntry* entry = &gs->atlasTileEntries[i];
- TileLookupKey key = { .bgDef = entry->bgDef, .srcX = entry->srcX, .srcY = entry->srcY, .srcW = entry->srcW, .srcH = entry->srcH };
- hmput(gs->tileEntryMap, key, entry);
- }
-
- gs->atlasBpp = safeCalloc(gs->atlasCount, sizeof(uint8_t));
- gs->atlasToChunk = safeMalloc(gs->atlasCount * sizeof(int16_t));
- repeat(gs->atlasCount, i) {
- gs->atlasToChunk[i] = -1;
- }
-
- // Build bpp table from TPAG and tile entries
- repeat(gs->atlasTPAGCount, i) {
- AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[i];
- if (entry->atlasId != 0xFFFF && gs->atlasCount > entry->atlasId) {
- gs->atlasBpp[entry->atlasId] = entry->bpp;
- }
- }
- repeat(gs->atlasTileCount, i) {
- AtlasTileEntry* entry = &gs->atlasTileEntries[i];
- if (entry->atlasId != 0xFFFF && gs->atlasCount > entry->atlasId) {
- gs->atlasBpp[entry->atlasId] = entry->bpp;
- }
- }
-
- fprintf(stderr, "GsRenderer: ATLAS.BIN loaded - %u TPAG entries, %u tile entries, %u atlases\n", gs->atlasTPAGCount, gs->atlasTileCount, gs->atlasCount);
-
- free(atlasBinPath);
-}
-
-// ===[ CLUT Loading and VRAM Upload ]===
-// Each CLUT is uploaded individually to its own VRAM address. This is necessary because
-// the PS2 GS VRAM has a block-swizzled layout - bulk-uploading stacked CLUTs and computing
-// linear offsets for CBP does NOT work (the BITBLT write path and CLUT read path use
-// block-based addressing, so CLUTs don't land at simple linear offsets within a bulk upload).
-static void loadAndUploadCLUTs(GsRenderer* gs) {
- GSGLOBAL* gsGlobal = gs->gsGlobal;
-
- // 128-byte aligned temp buffer for DMA transfers (reused for each CLUT send)
- // Large enough for one 8bpp CLUT (1024 bytes)
- uint8_t* tempBuf = (uint8_t*) safeMemalign(128, CLUT8_ENTRY_SIZE);
-
- // Load and upload CLUT4 (4bpp palettes: 16 colors * 4 bytes = 64 bytes each)
- {
- uint32_t clut4FileSize;
- uint8_t* clut4Data = loadFileRaw("CLUT4.BIN", &clut4FileSize);
- gs->clut4Count = clut4FileSize / CLUT4_ENTRY_SIZE;
- fprintf(stderr, "GsRenderer: CLUT4.BIN loaded - %u CLUTs (%u bytes)\n", gs->clut4Count, clut4FileSize);
-
- gs->clut4VramAddrs = safeMalloc(gs->clut4Count * sizeof(uint32_t));
-
- repeat(gs->clut4Count, i) {
- // gsKit uploads 4bpp CLUTs as 8x2 CT32 (16 entries in 8-wide, 2-tall grid)
- uint32_t vramSize = gsKit_texture_size(8, 2, GS_PSM_CT32);
- uint32_t vramAddr = gsKit_vram_alloc(gsGlobal, vramSize, GSKIT_ALLOC_USERBUFFER);
- if (vramAddr == GSKIT_ALLOC_ERROR) {
- fprintf(stderr, "GsRenderer: Failed to allocate VRAM for CLUT4 index %u\n", i);
- abort();
- }
-
- // Copy to aligned temp buffer for DMA
- memcpy(tempBuf, clut4Data + i * CLUT4_ENTRY_SIZE, CLUT4_ENTRY_SIZE);
- gsKit_texture_send((u32*) tempBuf, 8, 2, vramAddr, GS_PSM_CT32, 1, GS_CLUT_PALLETE);
- gs->clut4VramAddrs[i] = vramAddr;
- }
-
- fprintf(stderr, "GsRenderer: CLUT4 uploaded (%u CLUTs)\n", gs->clut4Count);
- free(clut4Data);
- }
-
- // Load and upload CLUT8 (8bpp palettes: 256 colors * 4 bytes = 1024 bytes each)
- {
- uint32_t clut8FileSize;
- uint8_t* clut8Data = loadFileRaw("CLUT8.BIN", &clut8FileSize);
- gs->clut8Count = clut8FileSize / CLUT8_ENTRY_SIZE;
- fprintf(stderr, "GsRenderer: CLUT8.BIN loaded - %u CLUTs (%u bytes)\n", gs->clut8Count, clut8FileSize);
-
- gs->clut8VramAddrs = safeMalloc(gs->clut8Count * sizeof(uint32_t));
-
- repeat(gs->clut8Count, i) {
- // gsKit uploads 8bpp CLUTs as 16x16 CT32 (256 entries in 16-wide, 16-tall grid)
- uint32_t vramSize = gsKit_texture_size(16, 16, GS_PSM_CT32);
- uint32_t vramAddr = gsKit_vram_alloc(gsGlobal, vramSize, GSKIT_ALLOC_USERBUFFER);
- if (vramAddr == GSKIT_ALLOC_ERROR) {
- fprintf(stderr, "GsRenderer: Failed to allocate VRAM for CLUT8 index %u\n", i);
- abort();
- }
-
- // 8bpp CLUTs are 1024 bytes; source is 128-byte aligned (1024 is a multiple of 128)
- gsKit_texture_send((u32*) (clut8Data + i * CLUT8_ENTRY_SIZE), 16, 16, vramAddr, GS_PSM_CT32, 1, GS_CLUT_PALLETE);
- gs->clut8VramAddrs[i] = vramAddr;
- }
-
- fprintf(stderr, "GsRenderer: CLUT8 uploaded (%u CLUTs)\n", gs->clut8Count);
- free(clut8Data);
- }
-
- free(tempBuf);
-
- fprintf(stderr, "GsRenderer: VRAM after CLUTs: 0x%08X / 0x%08X\n", gsGlobal->CurrentPointer, GS_VRAM_SIZE);
-}
-
-// ===[ VRAM Texture Cache (Buddy System with LRU Eviction) ]===
-// Manages a pool of 128KB VRAM chunks for atlas textures.
-// 4bpp atlases use 1 chunk, 8bpp atlases use 2 consecutive chunks.
-
-#define FONTM_RESERVED_VRAM 65536 // 64KB reserved for gsKit's TexManager (FONTM debug overlay)
-
-// Initializes the chunk pool from the remaining VRAM after CLUTs.
-// Reserves 64KB at the end for gsKit's TexManager (used by FONTM).
-// Our chunk pool occupies the middle, between CLUTs and the FONTM region.
-//
-// VRAM layout: [Framebuffers] [CLUTs] [Chunk Pool ...] [64KB FONTM]
-static void initTextureCache(GsRenderer* gs) {
- gs->textureVramBase = gs->gsGlobal->CurrentPointer;
- uint32_t availableVram = GS_VRAM_SIZE - gs->textureVramBase - FONTM_RESERVED_VRAM;
- gs->chunkCount = availableVram / VRAM_CHUNK_SIZE;
-
- gs->chunks = safeMalloc(gs->chunkCount * sizeof(VRAMChunk));
- forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) {
- chunk->atlasId = -1;
- chunk->lastUsed = 0;
- }
-
- gs->frameCounter = 1;
-
- // Advance CurrentPointer past our chunk pool so the TexManager only
- // manages the 64KB FONTM region at the end of VRAM.
- gs->gsGlobal->CurrentPointer = gs->textureVramBase + gs->chunkCount * VRAM_CHUNK_SIZE;
- gsKit_TexManager_init(gs->gsGlobal);
-
- uint32_t fontmVram = GS_VRAM_SIZE - gs->gsGlobal->CurrentPointer;
- fprintf(stderr, "GsRenderer: Texture cache initialized - %u chunks (%u KB each), base 0x%08X, %u KB for textures, %u KB for FONTM\n", gs->chunkCount, VRAM_CHUNK_SIZE / 1024, gs->textureVramBase, gs->chunkCount * (VRAM_CHUNK_SIZE / 1024), fontmVram / 1024);
-}
-
-// Find the first run of consecutive free chunks.
-// Returns the index of the first chunk, or -1 if not found.
-static int32_t findConsecutiveFreeChunks(GsRenderer* gs, int chunksNeeded) {
- int consecutive = 0;
- forEachIndexed(VRAMChunk, chunk, i, gs->chunks, gs->chunkCount) {
- if (0 > chunk->atlasId) {
- consecutive++;
- if (consecutive >= chunksNeeded) {
- return (int32_t) (i - (uint32_t) chunksNeeded + 1);
- }
- } else {
- consecutive = 0;
- }
- }
- return -1;
-}
-
-// Count total free chunks.
-static uint32_t countFreeChunks(GsRenderer* gs) {
- uint32_t count = 0;
- forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) {
- if (0 > chunk->atlasId)
- count++;
- }
- return count;
-}
-
-// Find the atlas with the oldest lastUsed time (LRU victim).
-// Returns the atlasId, or -1 if no loaded atlases.
-static int16_t findLRUVictim(GsRenderer* gs, bool* wasUsedOnThisFrame) {
- uint64_t oldest = UINT64_MAX;
- int16_t victimAtlas = -1;
- forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) {
- if (chunk->atlasId >= 0 && oldest > chunk->lastUsed) {
- oldest = chunk->lastUsed;
- victimAtlas = chunk->atlasId;
- }
- }
- if (victimAtlas != -1)
- *wasUsedOnThisFrame = oldest == gs->frameCounter;
- return victimAtlas;
-}
-
-// Evict an atlas from the cache, freeing its chunk(s).
-static void evictAtlas(GsRenderer* gs, int16_t atlasId) {
- forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) {
- if (chunk->atlasId == atlasId) {
- chunk->atlasId = -1;
- chunk->lastUsed = 0;
- }
- }
-
- if (atlasId >= 0 && gs->atlasCount > (uint16_t) atlasId) {
- gs->atlasToChunk[atlasId] = -1;
- }
-
- uint32_t availableChunks = countFreeChunks(gs);
- rendererPrintf("GsRenderer: Evicted atlas %d from VRAM (available chunks = %d)\n", atlasId, availableChunks);
-}
-
-// Defragment the texture cache by evicting all loaded atlases.
-// They will be reloaded on-demand as needed during subsequent draw calls.
-static void defragTextureCache(GsRenderer* gs) {
- rendererPrintf("GsRenderer: Defragmenting VRAM texture cache...\n");
-
- forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) {
- chunk->atlasId = -1;
- chunk->lastUsed = 0;
- }
-
- repeat(gs->atlasCount, i) {
- gs->atlasToChunk[i] = -1;
- }
-
- rendererPrintf("GsRenderer: Defrag complete - all %u chunks freed\n", gs->chunkCount);
-}
-
-// Allocate consecutive chunks for an atlas. Evicts LRU victims or defrags if needed.
-// Returns the first chunk index, or -1 if VRAM is truly exhausted.
-static int32_t allocateChunks(GsRenderer* gs, int chunksNeeded) {
- // Attempt 1: find free consecutive chunks
- int32_t idx = findConsecutiveFreeChunks(gs, chunksNeeded);
- if (idx >= 0) return idx;
-
- // Attempt 2: evict LRU victims one at a time until space is found
- repeat(gs->chunkCount, attempts) {
- bool wasUsedOnThisFrame = false;
-
- int16_t victim = findLRUVictim(gs, &wasUsedOnThisFrame);
- if (0 > victim)
- break;
-
- // We only need to flush if the victim was used on this frame
- // If it wasn't, then we can evict with no care in the world
- if (wasUsedOnThisFrame) {
- rendererPrintf("GsRenderer: Flushing draw queue before VRAM evicting because atlas was used on the current frame\n");
- gs->evictedAtlasUsedInCurrentFrame = true;
- gsKit_queue_exec(gs->gsGlobal);
- }
-
- evictAtlas(gs, victim);
-
- idx = findConsecutiveFreeChunks(gs, chunksNeeded);
-
- if (idx >= 0)
- return idx;
- }
-
- // At this point we are lost, just flush and hope for the best
- gs->evictedAtlasUsedInCurrentFrame = true;
- rendererPrintf("GsRenderer: Flushing draw queue before VRAM defrag\n");
- gsKit_queue_exec(gs->gsGlobal);
-
- // Attempt 3: defrag - evict ALL and let them reload on demand
- // Handles fragmentation where enough free chunks exist but aren't consecutive
- if (countFreeChunks(gs) >= (uint32_t) chunksNeeded) {
- defragTextureCache(gs);
- idx = findConsecutiveFreeChunks(gs, chunksNeeded);
-
- if (idx >= 0)
- return idx;
- }
-
- // VRAM truly exhausted
- return -1;
-}
-
-// ===[ EE RAM Atlas Cache (Bump Allocator with LRU Eviction + Compaction) ]===
-// Caches uncompressed atlas pixel data in a EE RAM buffer, allowingzero-copy DMA uploads to VRAM without per-upload decompression or temp allocations.
-
-#define EE_CACHE_CAPACITY (2 * 1024 * 1024) // 2 MiB
-
-// Uncompressed pixel data size for a 512x512 atlas at the given bpp.
-static uint32_t atlasUncompressedSize(uint8_t bpp) {
- return (bpp == 4) ? (ATLAS_WIDTH * ATLAS_HEIGHT / 2) : (ATLAS_WIDTH * ATLAS_HEIGHT);
-}
-
-// Decompress atlas pixel data from a compressed buffer (TEX_HEADER_SIZE header + RLE/raw payload).
-// Writes uncompressed indexed pixels into outBuf (must be large enough and 128-byte aligned).
-static void decompressAtlasPixels(const uint8_t* compressedData, uint8_t* outBuf) {
- uint16_t width = BinaryUtils_readUint16(compressedData + 1);
- uint16_t height = BinaryUtils_readUint16(compressedData + 3);
- uint8_t bpp = BinaryUtils_readUint8(compressedData + 5);
- uint32_t pixelDataSize = BinaryUtils_readUint32(compressedData + 6);
- uint8_t compressionType = BinaryUtils_readUint8(compressedData + 10);
-
- uint32_t uncompressedSize = (bpp == 4) ? (uint32_t) ((width * height + 1) / 2) : (uint32_t) (width * height);
- const uint8_t* rawData = compressedData + TEX_HEADER_SIZE;
-
- if (compressionType == 1) {
- // RLE decompression
- uint32_t srcPos = 0, dstPos = 0;
- while (pixelDataSize > srcPos + 1 && uncompressedSize > dstPos) {
- uint8_t runLength = rawData[srcPos++];
- uint8_t value = rawData[srcPos++];
- for (uint8_t j = 0; runLength > j && uncompressedSize > dstPos; j++) {
- outBuf[dstPos++] = value;
- }
- }
- } else {
- memcpy(outBuf, rawData, uncompressedSize);
- }
-}
-
-// Initialize the EE RAM cache. Called from gsInit after opening TEXTURES.BIN.
-static void initEeCache(GsRenderer* gs) {
- gs->eeCacheCapacity = EE_CACHE_CAPACITY;
- gs->eeCacheBumpPtr = 0;
- gs->eeCache = (uint8_t*) safeMemalign(128, EE_CACHE_CAPACITY);
-
- gs->eeCacheEntries = safeMalloc(gs->atlasCount * sizeof(EeAtlasCacheEntry));
- repeat(gs->atlasCount, i) {
- gs->eeCacheEntries[i].atlasId = -1;
- gs->eeCacheEntries[i].offset = 0;
- gs->eeCacheEntries[i].size = 0;
- gs->eeCacheEntries[i].lastUsed = 0;
- }
-
- // Compute on-disk sizes from offset table
- gs->atlasDataSizes = safeMalloc(gs->atlasCount * sizeof(uint32_t));
-
- // Get total file size for the last atlas
- fseek(gs->texturesFile, 0, SEEK_END);
- uint32_t texturesFileSize = (uint32_t) ftell(gs->texturesFile);
-
- repeat(gs->atlasCount, i) {
- if (gs->atlasCount - 1 > i) {
- gs->atlasDataSizes[i] = gs->atlasOffsets[i + 1] - gs->atlasOffsets[i];
- } else {
- gs->atlasDataSizes[i] = texturesFileSize - gs->atlasOffsets[i];
- }
- }
-}
-
-// Preload atlases sequentially into the EE cache until the buffer is full.
-// Reads compressed data from disc, decompresses, and stores uncompressed pixels in the cache.
-static void preloadEeCache(GsRenderer* gs) {
- uint32_t preloaded = 0;
-
- // Allocate a temp buffer for reading compressed data from disc
- uint32_t maxDiskSize = 0;
- repeat(gs->atlasCount, i) {
- if (gs->atlasDataSizes[i] > maxDiskSize) maxDiskSize = gs->atlasDataSizes[i];
- }
- uint8_t* tempBuf = (uint8_t*) safeMemalign(128, maxDiskSize);
-
- repeat(gs->atlasCount, i) {
- uint8_t bpp = gs->atlasBpp[i];
- uint32_t uncompSize = atlasUncompressedSize(bpp);
- if (gs->eeCacheBumpPtr + uncompSize > gs->eeCacheCapacity) {
- break;
- }
-
- // Read compressed data from disc into temp buffer
- uint32_t dataSize = gs->atlasDataSizes[i];
- fseek(gs->texturesFile, (long) gs->atlasOffsets[i], SEEK_SET);
- size_t bytesRead = fread(tempBuf, 1, dataSize, gs->texturesFile);
- if (bytesRead != dataSize) {
- fprintf(stderr, "GsRenderer: EE cache preload short read for atlas %u (expected %u, got %zu)\n", i, dataSize, bytesRead);
- break;
- }
-
- // Decompress directly into the EE cache
- decompressAtlasPixels(tempBuf, gs->eeCache + gs->eeCacheBumpPtr);
-
- gs->eeCacheEntries[i].atlasId = (int16_t) i;
- gs->eeCacheEntries[i].offset = gs->eeCacheBumpPtr;
- gs->eeCacheEntries[i].size = uncompSize;
- gs->eeCacheEntries[i].lastUsed = gs->frameCounter;
-
- gs->eeCacheBumpPtr += uncompSize;
- preloaded++;
- }
-
- free(tempBuf);
-
- fprintf(stderr, "GsRenderer: EE cache initialized - %u MB, %u atlases preloaded (%u KB used)\n", EE_CACHE_CAPACITY / (1024 * 1024), preloaded, gs->eeCacheBumpPtr / 1024);
-}
-
-// Look up an atlas in the EE cache. Returns pointer to cached data or nullptr.
-static uint8_t* eeCacheLookup(GsRenderer* gs, uint16_t atlasId) {
- if (atlasId >= gs->atlasCount) return nullptr;
- if (0 > gs->eeCacheEntries[atlasId].atlasId) return nullptr;
-
- gs->eeCacheEntries[atlasId].lastUsed = gs->frameCounter;
- return gs->eeCache + gs->eeCacheEntries[atlasId].offset;
-}
-
-// Compact the EE cache by closing gaps from evicted entries.
-static void compactEeCache(GsRenderer* gs) {
- // Collect live entries sorted by offset using insertion sort
- // (max 146 atlases, so a stack array + insertion sort is fine)
- uint16_t liveIds[256]; // More than enough for 146 atlases
- uint32_t liveCount = 0;
-
- repeat(gs->atlasCount, i) {
- if (gs->eeCacheEntries[i].atlasId >= 0) {
- // Insertion sort by offset
- uint32_t insertPos = liveCount;
- while (insertPos > 0 && gs->eeCacheEntries[liveIds[insertPos - 1]].offset > gs->eeCacheEntries[i].offset) {
- liveIds[insertPos] = liveIds[insertPos - 1];
- insertPos--;
- }
- liveIds[insertPos] = (uint16_t) i;
- liveCount++;
- }
- }
-
- // Walk and memmove each entry down to close gaps
- uint32_t writePtr = 0;
- repeat(liveCount, i) {
- EeAtlasCacheEntry* entry = &gs->eeCacheEntries[liveIds[i]];
- if (entry->offset != writePtr) {
- memmove(gs->eeCache + writePtr, gs->eeCache + entry->offset, entry->size);
- entry->offset = writePtr;
- }
- writePtr += entry->size;
- }
-
- gs->eeCacheBumpPtr = writePtr;
-}
-
-// Evict LRU entries until spaceNeeded bytes are available. Returns true on success.
-static bool eeCacheEvictLRU(GsRenderer* gs, uint32_t spaceNeeded) {
- // Calculate total live bytes to determine how much space we can free
- uint32_t liveBytes = 0;
- repeat(gs->atlasCount, i) {
- if (gs->eeCacheEntries[i].atlasId >= 0) {
- liveBytes += gs->eeCacheEntries[i].size;
- }
- }
-
- // Evict LRU entries until enough space would be freed after compaction
- while (gs->eeCacheCapacity - liveBytes < spaceNeeded) {
- // Find entry with smallest lastUsed
- uint64_t oldest = UINT64_MAX;
- int16_t victimId = -1;
-
- repeat(gs->atlasCount, i) {
- if (gs->eeCacheEntries[i].atlasId >= 0 && oldest > gs->eeCacheEntries[i].lastUsed) {
- oldest = gs->eeCacheEntries[i].lastUsed;
- victimId = (int16_t) i;
- }
- }
-
- if (0 > victimId) {
- break;
- }
-
- liveBytes -= gs->eeCacheEntries[victimId].size;
- gs->eeCacheEntries[victimId].atlasId = -1;
- }
-
- compactEeCache(gs);
-
- return gs->eeCacheCapacity - gs->eeCacheBumpPtr >= spaceNeeded;
-}
-
-// Insert atlas data into the EE cache. Evicts LRU entries if needed.
-static void eeCacheInsert(GsRenderer* gs, uint16_t atlasId, const uint8_t* data, uint32_t size) {
- if (size > gs->eeCacheCapacity) {
- // Atlas too large to ever fit in the cache
- return;
- }
-
- if (gs->eeCacheBumpPtr + size > gs->eeCacheCapacity) {
- if (!eeCacheEvictLRU(gs, size)) {
- rendererPrintf("GsRenderer: EE cache eviction failed for atlas %u (%u bytes)\n", atlasId, size);
- return;
- }
- }
-
- memcpy(gs->eeCache + gs->eeCacheBumpPtr, data, size);
-
- gs->eeCacheEntries[atlasId].atlasId = (int16_t) atlasId;
- gs->eeCacheEntries[atlasId].offset = gs->eeCacheBumpPtr;
- gs->eeCacheEntries[atlasId].size = size;
- gs->eeCacheEntries[atlasId].lastUsed = gs->frameCounter;
-
- gs->eeCacheBumpPtr += size;
-}
-
-// Upload atlas pixel data to the given VRAM chunk(s).
-// On cache hit: zero-copy DMA directly from EE cache (no decompression, no temp allocations).
-// On cache miss: reads compressed data from TEXTURES.BIN, decompresses, inserts into EE cache, then uploads.
-static void uploadAtlasToChunk(GsRenderer* gs, uint16_t atlasId, int32_t firstChunk) {
- uint8_t* uploadData = eeCacheLookup(gs, atlasId);
- uint8_t* tempPixelData = nullptr; // Non-null only if we need to free it after upload
- const char* atlasSource = "RAM";
-
- if (uploadData == nullptr) {
- // Cache miss: read compressed data from TEXTURES.BIN and decompress
- uint32_t dataSize = gs->atlasDataSizes[atlasId];
- uint8_t* compressedBuf = (uint8_t*) safeMemalign(128, dataSize);
-
- fseek(gs->texturesFile, (long) gs->atlasOffsets[atlasId], SEEK_SET);
- size_t bytesRead = fread(compressedBuf, 1, dataSize, gs->texturesFile);
- if (bytesRead != dataSize) {
- fprintf(stderr, "GsRenderer: Short read for atlas %u (expected %u, got %zu)\n", atlasId, dataSize, bytesRead);
- abort();
- }
-
- uint8_t bpp = gs->atlasBpp[atlasId];
- uint32_t uncompSize = atlasUncompressedSize(bpp);
- tempPixelData = (uint8_t*) safeMemalign(128, uncompSize);
- decompressAtlasPixels(compressedBuf, tempPixelData);
- free(compressedBuf);
-
- // Try to insert uncompressed data into EE cache
- eeCacheInsert(gs, atlasId, tempPixelData, uncompSize);
- atlasSource = "disk";
- gs->diskLoadsThisFrame++;
-
- uploadData = eeCacheLookup(gs, atlasId);
- if (uploadData != nullptr) {
- // Insert succeeded, use cached copy for DMA upload
- free(tempPixelData);
- tempPixelData = nullptr;
- } else {
- // EE cache insert failed, upload directly from temp buffer
- rendererPrintf("GsRenderer: EE cache insert failed for atlas %u, uploading directly\n", atlasId);
- uploadData = tempPixelData;
- }
- }
-
- // Upload pixel data to VRAM
- uint8_t bpp = gs->atlasBpp[atlasId];
- uint8_t psm = (bpp == 4) ? GS_PSM_T4 : GS_PSM_T8;
- uint32_t tbw = ATLAS_WIDTH / 64;
- uint32_t vramAddr = gs->textureVramBase + (uint32_t) firstChunk * VRAM_CHUNK_SIZE;
-
- gsKit_texture_send((u32*) uploadData, ATLAS_WIDTH, ATLAS_HEIGHT, vramAddr, psm, tbw, GS_CLUT_TEXTURE);
-
- // Update chunk state
- int chunksUsed = (bpp == 8) ? 2 : 1;
- repeat(chunksUsed, i) {
- gs->chunks[firstChunk + i].atlasId = (int16_t) atlasId;
- gs->chunks[firstChunk + i].lastUsed = gs->frameCounter;
- }
- gs->atlasToChunk[atlasId] = (int16_t) firstChunk;
-
- rendererPrintf("GsRenderer: Atlas %u uploaded to chunk %d (VRAM 0x%08X, %ubpp, src: %s)\n", atlasId, firstChunk, vramAddr, bpp, atlasSource);
-
- free(tempPixelData);
-}
-
-// Ensure an atlas is loaded into VRAM, using LRU eviction if needed.
-// Returns true on success, false on failure.
-static bool ensureAtlasLoaded(GsRenderer* gs, uint16_t atlasId) {
- if (atlasId >= gs->atlasCount) {
- fprintf(stderr, "GsRenderer: Atlas ID %u out of range (max %u)\n", atlasId, gs->atlasCount - 1);
- return false;
- }
-
- // Already loaded? Just touch LRU timestamp
- if (gs->atlasToChunk[atlasId] >= 0) {
- int16_t firstChunk = gs->atlasToChunk[atlasId];
- uint8_t bpp = gs->atlasBpp[atlasId];
- int chunksUsed = (bpp == 8) ? 2 : 1;
-
- // Track unique atlases per frame (first touch = lastUsed hasn't been updated yet)
- if (gs->chunks[firstChunk].lastUsed != gs->frameCounter) {
- gs->uniqueAtlasesThisFrame++;
- gs->chunksNeededThisFrame += (uint16_t) chunksUsed;
- }
-
- repeat(chunksUsed, i) {
- gs->chunks[firstChunk + i].lastUsed = gs->frameCounter;
- }
- return true;
- }
-
- // Determine how many chunks we need
- uint8_t bpp = gs->atlasBpp[atlasId];
- if (bpp != 4 && bpp != 8) {
- fprintf(stderr, "GsRenderer: Atlas %u has unknown bpp %u\n", atlasId, bpp);
- return false;
- }
- int chunksNeeded = (bpp == 8) ? 2 : 1;
-
- // Fresh load is always a new unique atlas this frame
- gs->uniqueAtlasesThisFrame++;
- gs->chunksNeededThisFrame += (uint16_t) chunksNeeded;
-
- // Allocate chunks (may evict or defrag)
- int32_t chunkIdx = allocateChunks(gs, chunksNeeded);
- if (0 > chunkIdx) {
- fprintf(stderr, "GsRenderer: VRAM exhausted! Cannot allocate %d chunk(s) for atlas %u (%ubpp)\n", chunksNeeded, atlasId, bpp);
- abort();
- }
-
- // Load TEX file and upload to the allocated chunk(s)
- uploadAtlasToChunk(gs, atlasId, chunkIdx);
- return true;
-}
-
-// ===[ GSTEXTURE setup for a given TPAG entry ]===
-// Configures a GSTEXTURE struct for rendering a specific atlas region.
-// The GSTEXTURE points to the atlas's VRAM location and the appropriate CLUT.
-static bool setupTextureForTPAG(GsRenderer* gs, GSTEXTURE* tex, int32_t tpagIndex) {
- if (0 > tpagIndex || (uint32_t) tpagIndex >= gs->atlasTPAGCount) return false;
-
- AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[tpagIndex];
- if (entry->atlasId == 0xFFFF) return false;
-
- // Ensure the atlas texture is loaded into VRAM (may trigger LRU eviction)
- if (!ensureAtlasLoaded(gs, entry->atlasId))
- return false;
-
- // Compute VRAM address from chunk index
- int16_t chunkIdx = gs->atlasToChunk[entry->atlasId];
- uint32_t vramAddr = gs->textureVramBase + (uint32_t) chunkIdx * VRAM_CHUNK_SIZE;
-
- memset(tex, 0, sizeof(GSTEXTURE));
- tex->Width = ATLAS_WIDTH;
- tex->Height = ATLAS_HEIGHT;
- tex->TBW = ATLAS_WIDTH / 64;
- tex->Vram = vramAddr;
- tex->Filter = GS_FILTER_NEAREST;
- tex->ClutStorageMode = GS_CLUT_STORAGE_CSM1;
-
- if (entry->bpp == 4) {
- tex->PSM = GS_PSM_T4;
- tex->ClutPSM = GS_PSM_CT32;
-
- if (entry->clutIndex >= gs->clut4Count) {
- fprintf(stderr, "GsRenderer: CLUT4 index %u out of range (max %u) for TPAG %d\n", entry->clutIndex, gs->clut4Count - 1, tpagIndex);
- abort();
- }
-
- tex->VramClut = gs->clut4VramAddrs[entry->clutIndex];
- } else {
- tex->PSM = GS_PSM_T8;
- tex->ClutPSM = GS_PSM_CT32;
-
- if (entry->clutIndex >= gs->clut8Count) {
- fprintf(stderr, "GsRenderer: CLUT8 index %u out of range (max %u) for TPAG %d\n", entry->clutIndex, gs->clut8Count - 1, tpagIndex);
- abort();
- }
-
- tex->VramClut = gs->clut8VramAddrs[entry->clutIndex];
- }
-
- return true;
-}
-
-// ===[ Tile Lookup and Texture Setup ]===
-
-// Finds a tile entry by (bgDef, srcX, srcY, srcW, srcH). Returns nullptr if not found.
-static AtlasTileEntry* findTileEntry(GsRenderer* gs, int16_t bgDef, uint16_t srcX, uint16_t srcY, uint16_t srcW, uint16_t srcH) {
- TileLookupKey key = { .bgDef = bgDef, .srcX = srcX, .srcY = srcY, .srcW = srcW, .srcH = srcH };
- ptrdiff_t idx = hmgeti(gs->tileEntryMap, key);
- if (idx == -1) return nullptr;
- return gs->tileEntryMap[idx].value;
-}
-
-// Configures a GSTEXTURE for rendering a tile entry. Same logic as setupTextureForTPAG but for AtlasTileEntry.
-static bool setupTextureForTile(GsRenderer* gs, GSTEXTURE* tex, AtlasTileEntry* entry) {
- if (entry->atlasId == 0xFFFF) return false;
-
- if (!ensureAtlasLoaded(gs, entry->atlasId))
- return false;
-
- int16_t chunkIdx = gs->atlasToChunk[entry->atlasId];
- uint32_t vramAddr = gs->textureVramBase + (uint32_t) chunkIdx * VRAM_CHUNK_SIZE;
-
- memset(tex, 0, sizeof(GSTEXTURE));
- tex->Width = ATLAS_WIDTH;
- tex->Height = ATLAS_HEIGHT;
- tex->TBW = ATLAS_WIDTH / 64;
- tex->Vram = vramAddr;
- tex->Filter = GS_FILTER_NEAREST;
- tex->ClutStorageMode = GS_CLUT_STORAGE_CSM1;
-
- if (entry->bpp == 4) {
- tex->PSM = GS_PSM_T4;
- tex->ClutPSM = GS_PSM_CT32;
-
- if (entry->clutIndex >= gs->clut4Count) {
- fprintf(stderr, "GsRenderer: CLUT4 index %u out of range (max %u) for tile (bg=%d)\n", entry->clutIndex, gs->clut4Count - 1, entry->bgDef);
- abort();
- }
-
- tex->VramClut = gs->clut4VramAddrs[entry->clutIndex];
- } else {
- tex->PSM = GS_PSM_T8;
- tex->ClutPSM = GS_PSM_CT32;
-
- if (entry->clutIndex >= gs->clut8Count) {
- fprintf(stderr, "GsRenderer: CLUT8 index %u out of range (max %u) for tile (bg=%d)\n", entry->clutIndex, gs->clut8Count - 1, entry->bgDef);
- abort();
- }
-
- tex->VramClut = gs->clut8VramAddrs[entry->clutIndex];
- }
-
- return true;
-}
-
-// ===[ Vtable Implementations ]===
-
-// Identity blend - source passes through unchanged. Used when GML disables blending but we still need PrimAlphaEnable=ON for TCC.
-// Equation: (Cs - 0) * 128/128 + 0 = Cs.
-// We do this because disabling blending (setting PrimAlphaEnable to OFF) makes GS stop honoring alpha writes from textures, which breaks masks.
-#define GS_ALPHA_NO_BLEND GS_SETREG_ALPHA(0, 2, 2, 2, 0x80)
-
-// Re-emits the FRAME_1 register with the current FBMSK. Called whenever the color write mask changes.
-static void gsApplyFBMask(GsRenderer* gs, u32 fbmsk) {
- GSGLOBAL* g = gs->gsGlobal;
- u64* p = (u64*) gsKit_heap_alloc(g, 1, 16, GIF_AD);
- *p++ = GIF_TAG_AD(1);
- *p++ = GIF_AD;
- *p++ = GS_SETREG_FRAME(g->ScreenBuffer[g->ActiveBuffer & 1] / 8192, g->Width / 64, g->PSM, fbmsk);
- *p++ = GS_FRAME_1 + g->PrimContext;
- gs->fbmsk = fbmsk;
-}
-
-// Re-emits the FBA_1 (Framebuffer Alpha) register. fba=1 forces bit 7 of the alpha to 1 at framebuffer writeback (after the blend equation has consumed As, so blending is unaffected).
-// fba=0 passes alpha through unchanged - required while the script is in alpha-only write mode so the intended mask value lands in FB.A verbatim.
-static void gsApplyFBA(GsRenderer* gs, uint8_t fba) {
- if (gs->fba == fba) return;
- GSGLOBAL* g = gs->gsGlobal;
- u64* p = (u64*) gsKit_heap_alloc(g, 1, 16, GIF_AD);
- *p++ = GIF_TAG_AD(1);
- *p++ = GIF_AD;
- *p++ = (u64) (fba & 1);
- *p++ = GS_FBA_1 + g->PrimContext;
- gs->fba = fba;
-}
-
-static void gsCommitBlend(GsRenderer* gs) {
- gsKit_set_primalpha(gs->gsGlobal, gs->blendEnabled ? gs->currentBlendAlpha : GS_ALPHA_NO_BLEND, 0);
-}
-
-static void gsInit(Renderer* renderer, DataWin* dataWin) {
- GsRenderer* gs = (GsRenderer*) renderer;
-
- renderer->dataWin = dataWin;
- renderer->drawColor = 0xFFFFFF;
- renderer->drawAlpha = 1.0f;
- renderer->drawFont = -1;
- renderer->drawHalign = 0;
- renderer->drawValign = 0;
-
- // Enable alpha blending
- gs->gsGlobal->PrimAlphaEnable = GS_SETTING_ON;
- gs->blendEnabled = true;
- gs->currentBlendAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0);
-
- // gsKit defaults Test->AREF to 0x80, but GMS's gpu_get_alphatestref() defaults to 0. Scripts that enable alpha test without calling gpu_set_alphatestref expect ref=0.
- // With ATST=GREATER and the post-MODULATE source alpha capped at 0x80, an AREF of 0x80 makes "0x80 > 0x80" fail, hiding all opaque pixels.
- gs->gsGlobal->Test->AREF = 0;
- gs->gsGlobal->Test->ATST = 6; // GREATER (matches GMS semantics)
- gs->gsGlobal->Test->AFAIL = 0; // KEEP
-
- // Force FB.A bit = 1 on every writeback via the GS FBA register so bm_dest_alpha / bm_inv_dest_alpha see opaque alpha for normal sprites.
- // This mimicks how OpenGL works
- gsApplyFBA(gs, 1);
-
- // Alpha blend: (Cs - Cd) * As / 128 + Cd (standard source-over)
- gsKit_set_primalpha(gs->gsGlobal, GS_SETREG_ALPHA(0, 1, 0, 1, 0), 0);
-
- // Load atlas metadata
- loadAtlas(gs);
-
- // Open TEXTURES.BIN and keep it open for on-demand atlas loading
- char* texturesBinPath = PS2Utils_createDevicePath("TEXTURES.BIN");
- gs->texturesFile = fopen(texturesBinPath, "rb");
- if (gs->texturesFile == nullptr) {
- fprintf(stderr, "GsRenderer: Failed to open %s\n", texturesBinPath);
- abort();
- }
- setvbuf(gs->texturesFile, nullptr, _IOFBF, 128 * 1024);
- free(texturesBinPath);
-
- // Upload CLUTs to VRAM
- loadAndUploadCLUTs(gs);
-
- // Initialize the texture cache chunk pool (uses remaining VRAM after CLUTs)
- initTextureCache(gs);
-
- // Initialize EE RAM cache for compressed atlas data
- initEeCache(gs);
- preloadEeCache(gs);
-
- fprintf(stderr, "GsRenderer: Initialized (textured mode)\n");
-}
-
-static void gsDestroy(Renderer* renderer) {
- GsRenderer* gs = (GsRenderer*) renderer;
- if (gs->texturesFile != nullptr) {
- fclose(gs->texturesFile);
- }
- free(gs->atlasOffsets);
- free(gs->atlasTPAGEntries);
- free(gs->atlasTileEntries);
- hmfree(gs->tileEntryMap);
- free(gs->chunks);
- free(gs->atlasToChunk);
- free(gs->atlasBpp);
- free(gs->clut4VramAddrs);
- free(gs->clut8VramAddrs);
- free(gs->eeCache);
- free(gs->eeCacheEntries);
- free(gs->atlasDataSizes);
- free(gs);
-}
-
-static void gsBeginFrame(Renderer* renderer, MAYBE_UNUSED int32_t gameW, MAYBE_UNUSED int32_t gameH, MAYBE_UNUSED int32_t windowW, MAYBE_UNUSED int32_t windowH) {
- GsRenderer* gs = (GsRenderer*) renderer;
- gs->frameCounter++;
- gs->evictedAtlasUsedInCurrentFrame = false;
- gs->uniqueAtlasesThisFrame = 0;
- gs->chunksNeededThisFrame = 0;
- gs->diskLoadsThisFrame = 0;
-
- // gsKit_setactive (called by sync_flip) re-emits FRAME with FBMSK=0, so any color-write mask we set last frame is gone. Re-apply it here for cases where GML leaves it asserted across frames.
- if (gs->fbmsk != 0) {
- gsApplyFBMask(gs, gs->fbmsk);
- }
-}
-
-static void gsEndFrame(MAYBE_UNUSED Renderer* renderer) {
- // No-op: flip happens in main loop
-}
-
-static void gsBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, MAYBE_UNUSED int32_t portW, MAYBE_UNUSED int32_t portH, MAYBE_UNUSED float viewAngle) {
- GsRenderer* gs = (GsRenderer*) renderer;
- gs->viewX = viewX;
- gs->viewY = viewY;
-
- // Scale game view to PS2 screen (640x448 NTSC interlaced)
- if (viewW > 0 && viewH > 0) {
- gs->scaleX = 640.0f / (float) viewW;
- gs->scaleY = gs->scaleX;
- } else {
- gs->scaleX = 2.0f;
- gs->scaleY = 2.0f;
- }
-
- // Center vertically
- float renderedH = (float) viewH * gs->scaleY;
- gs->offsetX = 0.0f;
- gs->offsetY = (448.0f - renderedH) / 2.0f;
-}
-
-static void gsEndView(MAYBE_UNUSED Renderer* renderer) {
- // No-op
-}
-
-static void gsBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, MAYBE_UNUSED int32_t portW, MAYBE_UNUSED int32_t portH) {
- GsRenderer* gs = (GsRenderer*) renderer;
- gs->viewX = 0;
- gs->viewY = 0;
-
- if (guiW > 0 && guiH > 0) {
- gs->scaleX = 640.0f / (float) guiW;
- gs->scaleY = gs->scaleX;
- } else {
- gs->scaleX = 2.0f;
- gs->scaleY = 2.0f;
- }
-
- float renderedH = (float) guiH * gs->scaleY;
- gs->offsetX = 0.0f;
- gs->offsetY = (448.0f - renderedH) / 2.0f;
-}
-
-static void gsEndGUI(MAYBE_UNUSED Renderer* renderer) {
- // No-op
-}
-
-static void gsDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return;
-
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
-
- // Get crop region from atlas entry (falls back to full bounding box if unmapped)
- float cropX = 0.0f, cropY = 0.0f;
- float cropW = (float) tpag->boundingWidth;
- float cropH = (float) tpag->boundingHeight;
- if (gs->atlasTPAGCount > (uint32_t) tpagIndex) {
- AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[tpagIndex];
- if (entry->atlasId != 0xFFFF) {
- cropX = (float) entry->cropX;
- cropY = (float) entry->cropY;
- cropW = (float) entry->cropW;
- cropH = (float) entry->cropH;
- }
- }
-
- // Compute 4 screen-space corners (tristrip Z-pattern: top-left, top-right, bottom-left, bottom-right)
- // sx0/sy0 = top-left, sx1/sy1 = top-right, sx2/sy2 = bottom-left, sx3/sy3 = bottom-right
- float sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
- bool hasRotation = angleDeg != 0.0f;
-
- if (hasRotation) {
- // Rotated: compute 4 transformed corners via matrix, same approach as the GLFW renderer
- // Position the cropped region within the original bounding box
- float localX0 = cropX - originX;
- float localY0 = cropY - originY;
- float localX1 = cropX + cropW - originX;
- float localY1 = cropY + cropH - originY;
-
- // Build 2D transform: T(x,y) * R(-angleDeg) * S(xscale, yscale)
- // Negate angle because Y-down coordinate system
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- Matrix4f transform;
- Matrix4f_setTransform2D(&transform, x, y, xscale, yscale, angleRad);
-
- float gx0, gy0, gx1, gy1, gx2, gy2, gx3, gy3;
- Matrix4f_transformPoint(&transform, localX0, localY0, &gx0, &gy0); // top-left
- Matrix4f_transformPoint(&transform, localX1, localY0, &gx1, &gy1); // top-right
- Matrix4f_transformPoint(&transform, localX0, localY1, &gx2, &gy2); // bottom-left
- Matrix4f_transformPoint(&transform, localX1, localY1, &gx3, &gy3); // bottom-right
-
- // Apply view offset and scale
- sx0 = (gx0 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy0 = (gy0 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- sx1 = (gx1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy1 = (gy1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- sx2 = (gx2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy2 = (gy2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- sx3 = (gx3 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy3 = (gy3 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- } else {
- // Axis-aligned: simple rect math
- // Position the cropped region within the original bounding box
- float gameX1 = x + (cropX - originX) * xscale;
- float gameY1 = y + (cropY - originY) * yscale;
- float gameX2 = x + (cropX + cropW - originX) * xscale;
- float gameY2 = y + (cropY + cropH - originY) * yscale;
-
- // Apply view offset and scale
- sx0 = (gameX1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy0 = (gameY1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- sx1 = (gameX2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy1 = (gameY1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- sx2 = (gameX1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy2 = (gameY2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- sx3 = (gameX2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- sy3 = (gameY2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- }
-
- // View frustum culling: skip if entirely off-screen (handles negative scales via min/max)
- float minSX = fminf(fminf(sx0, sx1), fminf(sx2, sx3));
- float maxSX = fmaxf(fmaxf(sx0, sx1), fmaxf(sx2, sx3));
- float minSY = fminf(fminf(sy0, sy1), fminf(sy2, sy3));
- float maxSY = fmaxf(fmaxf(sy0, sy1), fmaxf(sy2, sy3));
- if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT)
- return;
-
- // Set up GSTEXTURE for this TPAG entry
- GSTEXTURE tex;
- if (!setupTextureForTPAG(gs, &tex, tpagIndex)) {
- // Fallback: draw colored quad if no atlas mapping
- uint8_t r = BGR_R(color);
- uint8_t g = BGR_G(color);
- uint8_t b = BGR_B(color);
- uint8_t a = alphaToGS(alpha);
- u64 fallbackColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
- if (hasRotation) {
- gsKit_prim_quad(gs->gsGlobal, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3, 0, fallbackColor);
- } else {
- gsKit_prim_sprite(gs->gsGlobal, sx0, sy0, sx3, sy3, 0, fallbackColor);
- }
- return;
- }
-
- AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex];
-
- // The atlas entry has the actual sprite dimensions in the atlas (post-crop, post-resize).
- // The screen rect covers cropW x cropH game-space pixels, positioned at (cropX, cropY)
- // within the original bounding box. The GS hardware stretches the atlas texels to fill.
-
- // UV coords within the 512x512 atlas (in texels for gsKit)
- float u0 = (float) atlasEntry->atlasX;
- float v0 = (float) atlasEntry->atlasY;
- float u1 = u0 + (float) atlasEntry->width;
- float v1 = v0 + (float) atlasEntry->height;
-
- // GS modulate mode: Output = Texture * Vertex / 128
- // Scale vertex RGB from 0-255 to 0-128 so white (255) becomes 128 (1.0x multiplier)
- uint8_t r = BGR_R(color) >> 1;
- uint8_t g = BGR_G(color) >> 1;
- uint8_t b = BGR_B(color) >> 1;
- uint8_t a = alphaToGS(alpha);
- u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- if (hasRotation) {
- // Tristrip Z-pattern: needs 4 vertices for rotated quads
- gsKit_prim_quad_texture(
- gs->gsGlobal,
- &tex,
- sx0, sy0, u0, v0, // top-left
- sx1, sy1, u1, v0, // top-right
- sx2, sy2, u0, v1, // bottom-left
- sx3, sy3, u1, v1, // bottom-right
- 0,
- gsColor
- );
- } else {
- gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx3, sy3, u1, v1, 0, gsColor);
- }
-}
-
-static void gsDrawTiled(Renderer* renderer, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return;
-
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
-
- float cropX = 0.0f, cropY = 0.0f;
- float cropW = (float) tpag->boundingWidth;
- float cropH = (float) tpag->boundingHeight;
- if (gs->atlasTPAGCount > (uint32_t) tpagIndex) {
- AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[tpagIndex];
- if (entry->atlasId != 0xFFFF) {
- cropX = (float) entry->cropX;
- cropY = (float) entry->cropY;
- cropW = (float) entry->cropW;
- cropH = (float) entry->cropH;
- }
- }
-
- float axScale = fabsf(xscale);
- float ayScale = fabsf(yscale);
- float tileW = (float) tpag->boundingWidth * axScale;
- float tileH = (float) tpag->boundingHeight * ayScale;
- if (0 >= tileW || 0 >= tileH) return;
-
- float startX, endX, startY, endY;
- if (tileX) {
- startX = fmodf(x - originX * axScale, tileW);
- if (startX > 0) startX -= tileW;
- endX = roomW;
- } else {
- startX = x - originX * axScale;
- endX = startX + tileW;
- }
- if (tileY) {
- startY = fmodf(y - originY * ayScale, tileH);
- if (startY > 0) startY -= tileH;
- endY = roomH;
- } else {
- startY = y - originY * ayScale;
- endY = startY + tileH;
- }
-
- // Per-tile quad layout in world space, derived from gsDrawSprite's axis-aligned path.
- // For tile origin dx, the wrapper-equivalent x_call = dx + originX * axScale, then:
- // gameX1 = x_call + (cropX - originX) * xscale = dx + cropX * xscale + originX * (axScale - xscale)
- // Same shape for Y. Both reduce to dx + cropX*xscale when xscale > 0 (the common case).
- float dxLocalX0 = cropX * xscale + originX * (axScale - xscale);
- float dyLocalY0 = cropY * yscale + originY * (ayScale - yscale);
- float tileGameW = cropW * xscale;
- float tileGameH = cropH * yscale;
-
- GSTEXTURE tex;
- if (!setupTextureForTPAG(gs, &tex, tpagIndex)) return;
-
- AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex];
- float u0 = (float) atlasEntry->atlasX;
- float v0 = (float) atlasEntry->atlasY;
- float u1 = u0 + (float) atlasEntry->width;
- float v1 = v0 + (float) atlasEntry->height;
-
- // 0x80 is the exact 1.0x multiplier in GS modulate mode (output = texture * vertex / 128).
- // So, to avoid dimming the texture (BGR_R(0xFFFFFF) >> 1 = 0x7F is 0.992x) we'll hand-pick the white case.
- uint8_t r, g, b;
- if (color == 0xFFFFFFu) {
- r = g = b = 0x80;
- } else {
- r = BGR_R(color) >> 1;
- g = BGR_G(color) >> 1;
- b = BGR_B(color) >> 1;
- }
- uint8_t a = alphaToGS(alpha);
- u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- float viewBaseX = -(float) gs->viewX;
- float viewBaseY = -(float) gs->viewY;
- float viewScaleX = gs->scaleX;
- float viewScaleY = gs->scaleY;
- float viewOffX = gs->offsetX;
- float viewOffY = gs->offsetY;
-
- // Integer tile counts avoid FP-comparison drift; the inner break handles overshoot at the boundary
- int32_t tilesX = tileX ? ((int32_t) ((endX - startX) / tileW) + 1) : 1;
- int32_t tilesY = tileY ? ((int32_t) ((endY - startY) / tileH) + 1) : 1;
- if (0 >= tilesX || 0 >= tilesY) return;
-
- repeat(tilesY, iy) {
- float dy = startY + (float) iy * tileH;
- if (dy >= endY) break;
-
- float gameY1 = dy + dyLocalY0 + viewBaseY;
- float gameY2 = gameY1 + tileGameH;
- float sy0 = gameY1 * viewScaleY + viewOffY;
- float sy1 = gameY2 * viewScaleY + viewOffY;
-
- float minSY = sy0 < sy1 ? sy0 : sy1;
- float maxSY = sy0 > sy1 ? sy0 : sy1;
- if (0.0f > maxSY || minSY > PS2_SCREEN_HEIGHT) continue;
-
- repeat(tilesX, ix) {
- float dx = startX + (float) ix * tileW;
- if (endX <= dx) break;
-
- float gameX1 = dx + dxLocalX0 + viewBaseX;
- float gameX2 = gameX1 + tileGameW;
- float sx0 = gameX1 * viewScaleX + viewOffX;
- float sx1 = gameX2 * viewScaleX + viewOffX;
-
- float minSX = sx0 < sx1 ? sx0 : sx1;
- float maxSX = sx0 > sx1 ? sx0 : sx1;
- if (0.0f > maxSX || minSX > PS2_SCREEN_WIDTH) continue;
-
- gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx1, sy1, u1, v1, 0, gsColor);
- }
- }
-}
-
-static void gsDrawTiledPart(Renderer* renderer, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
- if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return;
-
- GSTEXTURE tex;
- if (!setupTextureForTPAG(gs, &tex, tpagIndex)) return;
-
- AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex];
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
-
- // Crop coords in source-page space (matching gsDrawSpritePart's coordinate system).
- float cX = (float) atlasEntry->cropX - (float) tpag->targetX;
- float cY = (float) atlasEntry->cropY - (float) tpag->targetY;
- float cW = (float) atlasEntry->cropW;
- float cH = (float) atlasEntry->cropH;
- float ratioX = cW > 0.0f ? (float) atlasEntry->width / cW : 1.0f;
- float ratioY = cH > 0.0f ? (float) atlasEntry->height / cH : 1.0f;
-
- uint8_t r, g, b;
- if (color == 0xFFFFFFu) { r = g = b = 0x80; } else { r = BGR_R(color) >> 1; g = BGR_G(color) >> 1; b = BGR_B(color) >> 1; }
- uint8_t a = alphaToGS(alpha);
- u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- float viewBaseX = -(float) gs->viewX;
- float viewBaseY = -(float) gs->viewY;
- float viewScaleX = gs->scaleX;
- float viewScaleY = gs->scaleY;
- float viewOffX = gs->offsetX;
- float viewOffY = gs->offsetY;
-
- int32_t tilesY = (int32_t)(dstH / (float) srcH) + 2;
- int32_t tilesX = (int32_t)(dstW / (float) srcW) + 2;
-
- repeat(tilesY, iy) {
- float rowDstY = dstY + (float) iy * (float) srcH;
- if (rowDstY >= dstY + dstH) break;
- int32_t rowSrcH = srcH < (int32_t)((dstY + dstH) - rowDstY) ? srcH : (int32_t)((dstY + dstH) - rowDstY);
-
- float intY1 = cY > (float) srcY ? cY : (float) srcY;
- float intY2 = (cY + cH) < (float)(srcY + rowSrcH) ? (cY + cH) : (float)(srcY + rowSrcH);
- if (intY1 >= intY2) continue;
- float clipOffY = intY1 - (float) srcY;
- float visH = intY2 - intY1;
- float v0 = (float) atlasEntry->atlasY + (intY1 - cY) * ratioY;
- float v1 = v0 + visH * ratioY;
-
- float sy0 = (rowDstY + clipOffY + viewBaseY) * viewScaleY + viewOffY;
- float sy1 = sy0 + visH * viewScaleY;
- float minSY = sy0 < sy1 ? sy0 : sy1;
- float maxSY = sy0 > sy1 ? sy0 : sy1;
- if (0.0f > maxSY || minSY > PS2_SCREEN_HEIGHT) continue;
-
- repeat(tilesX, ix) {
- float colDstX = dstX + (float) ix * (float) srcW;
- if (colDstX >= dstX + dstW) break;
- int32_t colSrcW = srcW < (int32_t)((dstX + dstW) - colDstX) ? srcW : (int32_t)((dstX + dstW) - colDstX);
-
- float intX1 = cX > (float) srcX ? cX : (float) srcX;
- float intX2 = (cX + cW) < (float)(srcX + colSrcW) ? (cX + cW) : (float)(srcX + colSrcW);
- if (intX1 >= intX2) continue;
- float clipOffX = intX1 - (float) srcX;
- float visW = intX2 - intX1;
- float u0 = (float) atlasEntry->atlasX + (intX1 - cX) * ratioX;
- float u1 = u0 + visW * ratioX;
-
- float sx0 = (colDstX + clipOffX + viewBaseX) * viewScaleX + viewOffX;
- float sx1 = sx0 + visW * viewScaleX;
- float minSX = sx0 < sx1 ? sx0 : sx1;
- float maxSX = sx0 > sx1 ? sx0 : sx1;
- if (0.0f > maxSX || minSX > PS2_SCREEN_WIDTH) continue;
-
- gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx1, sy1, u1, v1, 0, gsColor);
- }
- }
-}
-
-static void gsDrawSpritePart(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
-
- if (0 > tpagIndex || (uint32_t) tpagIndex >= renderer->dataWin->tpag.count) return;
-
- // Set up GSTEXTURE for this TPAG entry
- GSTEXTURE tex;
- bool hasTexture = setupTextureForTPAG(gs, &tex, tpagIndex);
-
- AtlasTPAGEntry* atlasEntry = hasTexture ? &gs->atlasTPAGEntries[tpagIndex] : nullptr;
- TexturePageItem* tpag = &renderer->dataWin->tpag.items[tpagIndex];
-
- // srcOffX/srcOffY are in source-page space (Renderer_drawSpritePartExt subtracts tpag->targetX/Y to convert from GML sprite-bounding space).
- // The preprocessor's cropX/cropY, however, are in sprite-bounding space (extractFromTPAG builds a boundingWidth x boundingHeight image with pixels offset by targetX/targetY, then cropTransparentBorders runs on that).
- // Subtract targetX/targetY here so both sides of the intersection live in the same coordinate system.
- float cX = hasTexture ? ((float) atlasEntry->cropX - (float) tpag->targetX) : 0.0f;
- float cY = hasTexture ? ((float) atlasEntry->cropY - (float) tpag->targetY) : 0.0f;
- float cW = hasTexture ? (float) atlasEntry->cropW : (float) tpag->sourceWidth;
- float cH = hasTexture ? (float) atlasEntry->cropH : (float) tpag->sourceHeight;
-
- float intX1 = fmaxf((float) srcOffX, cX);
- float intY1 = fmaxf((float) srcOffY, cY);
- float intX2 = fminf((float)(srcOffX + srcW), cX + cW);
- float intY2 = fminf((float)(srcOffY + srcH), cY + cH);
-
- if (intX1 >= intX2 || intY1 >= intY2) return;
-
- // Compute clip offset and visible region dimensions
- float clipOffX = intX1 - (float) srcOffX;
- float clipOffY = intY1 - (float) srcOffY;
- float visW = intX2 - intX1;
- float visH = intY2 - intY1;
-
- // World-space corners of the visible sub-rect (before rotation)
- float gx0 = x + clipOffX * xscale;
- float gy0 = y + clipOffY * yscale;
- float gx1 = gx0 + visW * xscale;
- float gy1 = gy0;
- float gx2 = gx0;
- float gy2 = gy0 + visH * yscale;
- float gx3 = gx1;
- float gy3 = gy2;
-
- if (angleDeg != 0.0f) {
- float angleRad = -angleDeg * ((float) M_PI / 180.0f);
- float cosA = cosf(angleRad);
- float sinA = sinf(angleRad);
- float dx, dy, rx, ry;
-#define ROTATE_CORNER(gxi, gyi) do { dx = (gxi) - pivotX; dy = (gyi) - pivotY; rx = cosA * dx - sinA * dy + pivotX; ry = sinA * dx + cosA * dy + pivotY; (gxi) = rx; (gyi) = ry; } while(0)
- ROTATE_CORNER(gx0, gy0);
- ROTATE_CORNER(gx1, gy1);
- ROTATE_CORNER(gx2, gy2);
- ROTATE_CORNER(gx3, gy3);
-#undef ROTATE_CORNER
- }
-
- // Convert game-space corners to screen space
- float sx0 = (gx0 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy0 = (gy0 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx1 = (gx1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (gy1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (gx2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (gy2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx3 = (gx3 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy3 = (gy3 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- // View frustum culling
- float minSX = fminf(fminf(sx0, sx1), fminf(sx2, sx3));
- float maxSX = fmaxf(fmaxf(sx0, sx1), fmaxf(sx2, sx3));
- float minSY = fminf(fminf(sy0, sy1), fminf(sy2, sy3));
- float maxSY = fmaxf(fmaxf(sy0, sy1), fmaxf(sy2, sy3));
- if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT) return;
-
- bool hasRotation = angleDeg != 0.0f;
-
- if (!hasTexture) {
- uint8_t r = BGR_R(color);
- uint8_t g = BGR_G(color);
- uint8_t b = BGR_B(color);
- uint8_t a = alphaToGS(alpha);
- u64 fallbackColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
- if (hasRotation) {
- gsKit_prim_quad(gs->gsGlobal, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3, 0, fallbackColor);
- } else {
- gsKit_prim_sprite(gs->gsGlobal, sx0, sy0, sx3, sy3, 0, fallbackColor);
- }
- return;
- }
-
- // Map intersection region to atlas UV space
- float ratioX = (cW > 0) ? ((float) atlasEntry->width / cW) : 1.0f;
- float ratioY = (cH > 0) ? ((float) atlasEntry->height / cH) : 1.0f;
-
- float u0 = (float) atlasEntry->atlasX + (intX1 - cX) * ratioX;
- float v0 = (float) atlasEntry->atlasY + (intY1 - cY) * ratioY;
- float u1 = u0 + visW * ratioX;
- float v1 = v0 + visH * ratioY;
-
- // GS modulate mode: Output = Texture * Vertex / 128
- uint8_t r = BGR_R(color) >> 1;
- uint8_t g = BGR_G(color) >> 1;
- uint8_t b = BGR_B(color) >> 1;
- uint8_t a = alphaToGS(alpha);
- u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- if (hasRotation) {
- gsKit_prim_quad_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx1, sy1, u1, v0, sx2, sy2, u0, v1, sx3, sy3, u1, v1, 0, gsColor);
- } else {
- gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx3, sy3, u1, v1, 0, gsColor);
- }
-}
-
-static void gsDrawSpritePos(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return;
-
- // Apply view transform. Z-pattern tristrip ordering: (0)=TL, (1)=TR, (2)=BL, (3)=BR.
- float sx0 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy0 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx1 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (x4 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (y4 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx3 = (x3 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy3 = (y3 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- float minSX = fminf(fminf(sx0, sx1), fminf(sx2, sx3));
- float maxSX = fmaxf(fmaxf(sx0, sx1), fmaxf(sx2, sx3));
- float minSY = fminf(fminf(sy0, sy1), fminf(sy2, sy3));
- float maxSY = fmaxf(fmaxf(sy0, sy1), fmaxf(sy2, sy3));
- if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT) return;
-
- GSTEXTURE tex;
- if (!setupTextureForTPAG(gs, &tex, tpagIndex)) {
- uint8_t a = alphaToGS(alpha);
- u64 fallbackColor = GS_SETREG_RGBAQ(0xFF, 0xFF, 0xFF, a, 0x00);
- gsKit_prim_quad(gs->gsGlobal, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3, 0, fallbackColor);
- return;
- }
-
- AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex];
-
- // Map the entire atlas entry (the trimmed source content in atlas texels) to the user's quad.
- float u0 = (float) atlasEntry->atlasX;
- float v0 = (float) atlasEntry->atlasY;
- float u1 = u0 + (float) atlasEntry->width;
- float v1 = v0 + (float) atlasEntry->height;
-
- // GS modulate mode: Output = Texture * Vertex / 128
- uint8_t a = alphaToGS(alpha);
- u64 gsColor = GS_SETREG_RGBAQ(0x80, 0x80, 0x80, a, 0x00);
-
- gsKit_prim_quad_texture(
- gs->gsGlobal,
- &tex,
- sx0, sy0, u0, v0, // TL
- sx1, sy1, u1, v0, // TR
- sx2, sy2, u0, v1, // BL
- sx3, sy3, u1, v1, // BR
- 0,
- gsColor
- );
-}
-
-static void gsDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) {
- GsRenderer* gs = (GsRenderer*) renderer;
-
- uint8_t r = BGR_R(color);
- uint8_t g = BGR_G(color);
- uint8_t b = BGR_B(color);
- uint8_t a = alphaToGS(alpha);
-
- float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- u64 rectColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- if (outline) {
- // Draw 4 one-pixel-wide edges: top, bottom, left, right
- float pw = gs->scaleX; // one pixel width in screen coords
- float ph = gs->scaleY; // one pixel height in screen coords
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2 + pw, sy1 + ph, 0, rectColor); // top
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy2, sx2 + pw, sy2 + ph, 0, rectColor); // bottom
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy1 + ph, sx1 + pw, sy2, 0, rectColor); // left
- gsKit_prim_sprite(gs->gsGlobal, sx2, sy1 + ph, sx2 + pw, sy2, 0, rectColor); // right
- } else {
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, rectColor);
- }
-}
-
-static void gsDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, MAYBE_UNUSED float width, uint32_t color, float alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
-
- uint8_t r = BGR_R(color);
- uint8_t g = BGR_G(color);
- uint8_t b = BGR_B(color);
- uint8_t a = alphaToGS(alpha);
-
- float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- u64 lineColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
- gsKit_prim_line(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, lineColor);
-}
-
-// PS2 gsKit doesn't support per-vertex colors on lines, so we just use color1
-static void gsDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, MAYBE_UNUSED uint32_t color2, float alpha) {
- renderer->vtable->drawLine(renderer, x1, y1, x2, y2, width, color1, alpha);
-}
-
-// Resolved font state shared between gsDrawText and gsDrawTextColor
-typedef struct {
- Font* font;
- GSTEXTURE tex; // GL equivalent: GLuint texId + int32_t texW/texH
- AtlasTPAGEntry* atlasEntry; // GL equivalent: TexturePageItem* fontTpag
- float ratioX, ratioY; // atlas-to-original scale (GL doesn't need this, uses texW/texH directly)
- Sprite* spriteFontSprite; // source sprite for sprite fonts (nullptr for regular fonts)
-} GsFontState;
-
-// Resolves font texture state
-// Returns false if the font can't be drawn
-static bool gsResolveFontState(GsRenderer* gs, DataWin* dw, Font* font, GsFontState* state) {
- state->font = font;
- state->atlasEntry = nullptr;
- state->ratioX = 1.0f;
- state->ratioY = 1.0f;
- state->spriteFontSprite = nullptr;
-
- if (!font->isSpriteFont) {
- int32_t fontTpagIndex = font->tpagIndex;
- if (0 > fontTpagIndex) return false;
-
- if (!setupTextureForTPAG(gs, &state->tex, fontTpagIndex)) return false;
-
- state->atlasEntry = &gs->atlasTPAGEntries[fontTpagIndex];
- TexturePageItem* fontTpag = &dw->tpag.items[fontTpagIndex];
-
- float origW = (float) fontTpag->sourceWidth;
- float origH = (float) fontTpag->sourceHeight;
- state->ratioX = (origW > 0) ? ((float) state->atlasEntry->width / origW) : 1.0f;
- state->ratioY = (origH > 0) ? ((float) state->atlasEntry->height / origH) : 1.0f;
- } else if (font->spriteIndex >= 0 && dw->sprt.count > (uint32_t) font->spriteIndex) {
- state->spriteFontSprite = &dw->sprt.sprites[font->spriteIndex];
- }
- return true;
-}
-
-// Resolves UV coordinates, texture ID, and local position for a single glyph
-// Returns false if the glyph can't be drawn
-static bool gsResolveGlyph(GsRenderer* gs, DataWin* dw, GsFontState* state, FontGlyph* glyph, float cursorX, float cursorY, GSTEXTURE* outTex, float* outU0, float* outV0, float* outU1, float* outV1, float* outLocalX0, float* outLocalY0) {
- Font* font = state->font;
- if (font->isSpriteFont && state->spriteFontSprite != nullptr) {
- Sprite* sprite = state->spriteFontSprite;
- int32_t glyphIndex = (int32_t) (glyph - font->glyphs);
- if (0 > glyphIndex || glyphIndex >= (int32_t) sprite->textureCount) return false;
-
- int32_t tpagIdx = sprite->tpagIndices[glyphIndex];
- if (0 > tpagIdx) return false;
-
- if (!setupTextureForTPAG(gs, outTex, tpagIdx)) return false;
-
- AtlasTPAGEntry* glyphAtlas = &gs->atlasTPAGEntries[tpagIdx];
- TexturePageItem* glyphTpag = &dw->tpag.items[tpagIdx];
- float gOrigW = (float) glyphTpag->sourceWidth;
- float gOrigH = (float) glyphTpag->sourceHeight;
- float gRatioX = (gOrigW > 0) ? ((float) glyphAtlas->width / gOrigW) : 1.0f;
- float gRatioY = (gOrigH > 0) ? ((float) glyphAtlas->height / gOrigH) : 1.0f;
-
- *outU0 = (float) glyphAtlas->atlasX;
- *outV0 = (float) glyphAtlas->atlasY;
- *outU1 = *outU0 + (float) glyph->sourceWidth * gRatioX;
- *outV1 = *outV0 + (float) glyph->sourceHeight * gRatioY;
-
- *outLocalX0 = cursorX + (float) glyph->offset;
- *outLocalY0 = cursorY + (float) ((int32_t) glyphTpag->targetY - sprite->originY);
- } else {
- *outTex = state->tex;
-
- *outU0 = (float) state->atlasEntry->atlasX + (float) glyph->sourceX * state->ratioX;
- *outV0 = (float) state->atlasEntry->atlasY + (float) glyph->sourceY * state->ratioY;
- *outU1 = *outU0 + (float) glyph->sourceWidth * state->ratioX;
- *outV1 = *outV0 + (float) glyph->sourceHeight * state->ratioY;
-
- *outLocalX0 = cursorX + (float) glyph->offset;
- *outLocalY0 = cursorY;
- }
- return true;
-}
-
-static void gsDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg) {
- GsRenderer* gs = (GsRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > renderer->drawFont || (uint32_t) renderer->drawFont >= dw->font.count) return;
-
- Font* font = &dw->font.fonts[renderer->drawFont];
-
- GsFontState fontState;
- if (!gsResolveFontState(gs, dw, font, &fontState)) return;
-
- // GS modulate mode: Output = Texture * Vertex / 128
- // Scale vertex RGB from 0-255 to 0-128 so white (255) becomes 1.0x multiplier
- uint32_t color = renderer->drawColor;
- uint8_t a = alphaToGS(renderer->drawAlpha);
- uint8_t r = BGR_R(color) >> 1;
- uint8_t g = BGR_G(color) >> 1;
- uint8_t b = BGR_B(color) >> 1;
- u64 textColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- float screenScaleX = xscale * font->scaleX * gs->scaleX;
- float screenScaleY = yscale * font->scaleY * gs->scaleY;
- float screenBaseX = (x - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float screenBaseY = (y - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- int32_t textLen = (int32_t) strlen(text);
-
- // Vertical alignment
- int32_t lineCount = TextUtils_countLines(text, textLen);
- float lineStride = TextUtils_lineStride(font);
- float valignOffset = 0;
- if (renderer->drawValign != 0) {
- float totalHeight = (float) lineCount * lineStride;
- if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f;
- else if (renderer->drawValign == 2) valignOffset = -totalHeight;
- }
-
- float cursorY = valignOffset - (float) font->ascenderOffset;
- int32_t lineStart = 0;
-
- while (textLen >= lineStart) {
- // Find end of current line
- int32_t lineEnd = lineStart;
- while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) {
- lineEnd++;
- }
-
- int32_t lineLen = lineEnd - lineStart;
- const char* line = text + lineStart;
-
- // Horizontal alignment
- float halignOffset = 0;
- if (renderer->drawHalign != 0) {
- float lineWidth = TextUtils_measureLineWidth(font, line, lineLen);
- if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f;
- else if (renderer->drawHalign == 2) halignOffset = -lineWidth;
- }
-
- float cursorX = halignOffset;
-
- // Draw each glyph - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning)
- int32_t pos = 0;
- uint16_t ch = 0;
- bool hasCh = false;
- if (lineLen > pos) {
- ch = TextUtils_decodeUtf8(line, lineLen, &pos);
- hasCh = true;
- }
-
- while (hasCh) {
- FontGlyph* glyph = TextUtils_findGlyph(font, ch);
-
- uint16_t nextCh = 0;
- bool hasNext = lineLen > pos;
- if (hasNext) nextCh = TextUtils_decodeUtf8(line, lineLen, &pos);
-
- if (glyph != nullptr) {
- bool resolveOk = true;
- if (glyph->sourceWidth > 0 && glyph->sourceHeight > 0) {
- GSTEXTURE glyphTex;
- float u0 = 0, v0 = 0, u1 = 0, v1 = 0;
- float localX0, localY0;
-
- if (gsResolveGlyph(gs, dw, &fontState, glyph, cursorX, cursorY, &glyphTex, &u0, &v0, &u1, &v1, &localX0, &localY0)) {
- float sx1 = localX0 * screenScaleX + screenBaseX;
- float sy1 = localY0 * screenScaleY + screenBaseY;
- float sx2 = sx1 + (float) glyph->sourceWidth * screenScaleX;
- float sy2 = sy1 + (float) glyph->sourceHeight * screenScaleY;
-
- gsKit_prim_sprite_texture(gs->gsGlobal, &glyphTex, sx1, sy1, u0, v0, sx2, sy2, u1, v1, 0, textColor);
- } else {
- resolveOk = false;
- }
- }
-
- cursorX += (float) glyph->shift;
- if (resolveOk && hasNext) {
- cursorX += TextUtils_getKerningOffset(glyph, nextCh);
- }
- }
-
- ch = nextCh;
- hasCh = hasNext;
- }
-
- // Next line
- cursorY += lineStride;
- if (textLen > lineEnd) {
- lineStart = TextUtils_skipNewline(text, lineEnd, textLen);
- } else {
- break;
- }
- }
-}
-
-static void gsDrawTextColor(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg, int32_t _c1, int32_t _c2, int32_t _c3, int32_t _c4, float alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > renderer->drawFont || (uint32_t) renderer->drawFont >= dw->font.count) return;
-
- Font* font = &dw->font.fonts[renderer->drawFont];
-
- GsFontState fontState;
- if (!gsResolveFontState(gs, dw, font, &fontState)) return;
-
- int32_t textLen = (int32_t) strlen(text);
- if(textLen == 0) return;
-
- float screenScaleX = xscale * font->scaleX * gs->scaleX;
- float screenScaleY = yscale * font->scaleY * gs->scaleY;
- float screenBaseX = (x - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float screenBaseY = (y - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- // Vertical alignment
- int32_t lineCount = TextUtils_countLines(text, textLen);
- float lineStride = TextUtils_lineStride(font);
- float totalHeight = (float) lineCount * lineStride;
- float valignOffset = 0;
- if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f;
- else if (renderer->drawValign == 2) valignOffset = -totalHeight;
-
- float cursorY = valignOffset - (float) font->ascenderOffset;
- int32_t lineStart = 0;
-
- // get delta's (16.16 format)
- int32_t left_r_dx = ((_c2 & 0xff0000) - (_c1 & 0xff0000)) / textLen;
- int32_t left_g_dx = ((((_c2 & 0xff00) << 8) - ((_c1 & 0xff00) << 8))) / textLen;
- int32_t left_b_dx = ((((_c2 & 0xff) << 16) - ((_c1 & 0xff) << 16))) / textLen;
-
- int32_t right_r_dx = ((_c3 & 0xff0000) - (_c4 & 0xff0000)) / textLen;
- int32_t right_g_dx = ((((_c3 & 0xff00) << 8) - ((_c4 & 0xff00) << 8))) / textLen;
- int32_t right_b_dx = ((((_c3 & 0xff) << 16) - ((_c4 & 0xff) << 16))) / textLen;
-
- int32_t left_delta_r = left_r_dx;
- int32_t left_delta_g = left_g_dx;
- int32_t left_delta_b = left_b_dx;
- int32_t right_delta_r = right_r_dx;
- int32_t right_delta_g = right_g_dx;
- int32_t right_delta_b = right_b_dx;
-
- int32_t c1 = _c1;
- int32_t c4 = _c4;
-
- while (textLen >= lineStart) {
- // do 16.16 maths
- int32_t c2 = ((c1 & 0xff0000) + (left_delta_r & 0xff0000)) & 0xff0000;
- c2 |= ((c1 & 0xff00) + (left_delta_g >> 8) & 0xff00) & 0xff00;
- c2 |= ((c1 & 0xff) + (left_delta_b >> 16)) & 0xff;
- int32_t c3 = ((c4 & 0xff0000) + (right_delta_r & 0xff0000)) & 0xff0000;
- c3 |= ((c4 & 0xff00) + (right_delta_g >> 8) & 0xff00) & 0xff00;
- c3 |= ((c4 & 0xff) + (right_delta_b >> 16)) & 0xff;
-
- // GS modulate mode: Output = Texture * Vertex / 128
- // Scale vertex RGB from 0-255 to 0-128 so white (255) becomes 1.0x multiplier
- uint8_t ga = alphaToGS(alpha);
- uint8_t r1 = BGR_R(c1) >> 1;
- uint8_t g1 = BGR_G(c1) >> 1;
- uint8_t b1 = BGR_B(c1) >> 1;
- u64 textColor1 = GS_SETREG_RGBAQ(r1, g1, b1, ga, 0x00);
-
- uint8_t r2 = BGR_R(c2) >> 1;
- uint8_t g2 = BGR_G(c2) >> 1;
- uint8_t b2 = BGR_B(c2) >> 1;
- u64 textColor2 = GS_SETREG_RGBAQ(r2, g2, b2, ga, 0x00);
-
- uint8_t r3 = BGR_R(c3) >> 1;
- uint8_t g3 = BGR_G(c3) >> 1;
- uint8_t b3 = BGR_B(c3) >> 1;
- u64 textColor3 = GS_SETREG_RGBAQ(r3, g3, b3, ga, 0x00);
-
- uint8_t r4 = BGR_R(c4) >> 1;
- uint8_t g4 = BGR_G(c4) >> 1;
- uint8_t b4 = BGR_B(c4) >> 1;
- u64 textColor4 = GS_SETREG_RGBAQ(r4, g4, b4, ga, 0x00);
-
- left_delta_r += left_r_dx;
- left_delta_g += left_g_dx;
- left_delta_b += left_b_dx;
- right_delta_r += right_r_dx;
- right_delta_g += right_g_dx;
- right_delta_b += right_b_dx;
-
- // Find end of current line
- int32_t lineEnd = lineStart;
- while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) {
- lineEnd++;
- }
-
- int32_t lineLen = lineEnd - lineStart;
- const char* line = text + lineStart;
-
- // Horizontal alignment
- float lineWidth = TextUtils_measureLineWidth(font, line, lineLen);
- float halignOffset = 0;
- if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f;
- else if (renderer->drawHalign == 2) halignOffset = -lineWidth;
-
- float cursorX = halignOffset;
-
- // Draw each glyph - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning)
- int32_t pos = 0;
- uint16_t ch = 0;
- bool hasCh = false;
- if (lineLen > pos) {
- ch = TextUtils_decodeUtf8(line, lineLen, &pos);
- hasCh = true;
- }
-
- while (hasCh) {
- FontGlyph* glyph = TextUtils_findGlyph(font, ch);
-
- uint16_t nextCh = 0;
- bool hasNext = lineLen > pos;
- if (hasNext) nextCh = TextUtils_decodeUtf8(line, lineLen, &pos);
-
- if (glyph != nullptr) {
- bool resolveOk = true;
- if (glyph->sourceWidth > 0 && glyph->sourceHeight > 0) {
- GSTEXTURE glyphTex;
- float u0 = 0, v0 = 0, u1 = 0, v1 = 0;
- float localX0, localY0;
-
- if (gsResolveGlyph(gs, dw, &fontState, glyph, cursorX, cursorY, &glyphTex, &u0, &v0, &u1, &v1, &localX0, &localY0)) {
- float sx1 = localX0 * screenScaleX + screenBaseX;
- float sy1 = localY0 * screenScaleY + screenBaseY;
- float sx2 = sx1 + (float) glyph->sourceWidth * screenScaleX;
- float sy2 = sy1 + (float) glyph->sourceHeight * screenScaleY;
-
- gsKit_prim_triangle_goraud_texture_3d(gs->gsGlobal, &glyphTex,
- sx1, sy1, 0, u0, v0,
- sx2, sy1, 0, u1, v0,
- sx2, sy2, 0, u1, v1,
- textColor1, textColor2, textColor3);
- gsKit_prim_triangle_goraud_texture_3d(gs->gsGlobal, &glyphTex,
- sx1, sy1, 0, u0, v0,
- sx2, sy2, 0, u1, v1,
- sx1, sy2, 0, u0, v1,
- textColor1, textColor3, textColor4);
- } else {
- resolveOk = false;
- }
- }
-
- cursorX += (float) glyph->shift;
- if (resolveOk && hasNext) {
- cursorX += TextUtils_getKerningOffset(glyph, nextCh);
- }
- }
-
- ch = nextCh;
- hasCh = hasNext;
- }
-
- // Next line
- cursorY += lineStride;
- if (textLen > lineEnd) {
- lineStart = TextUtils_skipNewline(text, lineEnd, textLen);
- } else {
- break;
- }
- c4 = c3; // set left edge to be what the last right edge was....
- c1 = c2; //
- }
-}
-
-static void gsDrawTriangle(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline)
-{
- GsRenderer* gs = (GsRenderer*) renderer;
- if(outline)
- {
- gsDrawLine(renderer, x1, y1, x2, y2, 1, renderer->drawColor, 1.0);
- gsDrawLine(renderer, x2, y2, x3, y3, 1, renderer->drawColor, 1.0);
- gsDrawLine(renderer, x3, y3, x1, y1, 1, renderer->drawColor, 1.0);
- } else {
- float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx3 = (x3 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy3 = (y3 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- float r = (float) BGR_R(renderer->drawColor);
- float g = (float) BGR_G(renderer->drawColor);
- float b = (float) BGR_B(renderer->drawColor);
-
- u64 triColor = GS_SETREG_RGBAQ(r, g, b, alphaToGS(renderer->drawAlpha), 0x00);
- gsKit_prim_triangle_gouraud_3d(gs->gsGlobal, sx1, sy1, 0,sx2, sy2, 0,sx3, sy3, 0,triColor, triColor, triColor);
- }
-}
-
-static void gsFlush(MAYBE_UNUSED Renderer* renderer) {
- // No-op: gsKit queues commands, executed in main loop
-}
-
-static int32_t gsCreateSpriteFromSurface(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t x, MAYBE_UNUSED int32_t y, MAYBE_UNUSED int32_t w, MAYBE_UNUSED int32_t h, MAYBE_UNUSED bool removeback, MAYBE_UNUSED bool smooth, MAYBE_UNUSED int32_t xorig, MAYBE_UNUSED int32_t yorig) {
- rendererPrintf("GsRenderer: createSpriteFromSurface not supported on PS2\n");
- return -1;
-}
-
-static void gsDeleteSprite(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t spriteIndex) {
- // No-op
-}
-
-// PS2 GS only supports a single blend equation:
-// Cv = (A - B) * (C / 128) + D
-// Where A, B, D pick from {Cs=0, Cd=1, 0=2} and C picks from {As=0, Ad=1, FIX=2}.
-// This is a much smaller space than GL's sf*Cs + df*Cd, so most non-trivial GMS blend modes get approximated.
-// The cases that DO map exactly are the ones DELTARUNE relies on for the dest-alpha mask trick (bm_dest_alpha + bm_inv_dest_alpha) and standard alpha blending (bm_normal).
-// Anything we cannot express falls back to bm_normal.
-
-// Builds a GS_SETREG_ALPHA value from (sfactor, dfactor) pair semantics: result = sf*Cs + df*Cd.
-// Returns true on exact match, false if the pair was approximated.
-static bool gmsFactorPairToGSAlpha(int32_t sf, int32_t df, u64* outAlpha) {
- // (src_alpha, inv_src_alpha) -> (Cs - Cd) * As + Cd
- if (sf == bm_src_alpha && df == bm_inv_src_alpha) { *outAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0); return true; }
- // (src_alpha, one) -> Cs*As + Cd
- if (sf == bm_src_alpha && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(0, 2, 0, 1, 0); return true; }
- // (one, one) -> Cs + Cd (FIX=128 makes C/128 == 1)
- if (sf == bm_one && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(0, 2, 2, 1, 0x80); return true; }
- // (one, inv_src_alpha) -> approximate as bm_normal (premultiplied alpha case)
- if (sf == bm_one && df == bm_inv_src_alpha) { *outAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0); return true; }
- // (zero, one) -> Cd (no source contribution)
- if (sf == bm_zero && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(2, 2, 2, 1, 0); return true; }
- // (one, zero) -> Cs (replace dest)
- if (sf == bm_one && df == bm_zero) { *outAlpha = GS_SETREG_ALPHA(0, 2, 2, 2, 0x80); return true; }
- // (zero, zero) -> 0 (clear)
- if (sf == bm_zero && df == bm_zero) { *outAlpha = GS_SETREG_ALPHA(2, 2, 2, 2, 0); return true; }
- // (dest_alpha, inv_dest_alpha) -> (Cs - Cd) * Ad + Cd
- if (sf == bm_dest_alpha && df == bm_inv_dest_alpha) { *outAlpha = GS_SETREG_ALPHA(0, 1, 1, 1, 0); return true; }
- // (inv_dest_alpha, dest_alpha) -> (Cd - Cs) * Ad + Cs
- if (sf == bm_inv_dest_alpha && df == bm_dest_alpha) { *outAlpha = GS_SETREG_ALPHA(1, 0, 1, 0, 0); return true; }
- // (dest_alpha, one) -> Cs*Ad + Cd
- if (sf == bm_dest_alpha && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(0, 2, 1, 1, 0); return true; }
- // (zero, src_alpha) -> Cd*As (modulate dest by source alpha)
- if (sf == bm_zero && df == bm_src_alpha) { *outAlpha = GS_SETREG_ALPHA(2, 1, 0, 1, 0); return true; }
- // (zero, inv_src_alpha) -> Cd * (1 - As) -> (0 - Cd) * As + Cd
- if (sf == bm_zero && df == bm_inv_src_alpha) { *outAlpha = GS_SETREG_ALPHA(2, 1, 0, 1, 0); return false; } // off-by-(approximation), tolerable
-
- // Fallback: behave like bm_normal so things stay roughly visible.
- *outAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0);
- return false;
-}
-
-// Maps the simple GMS blend modes (bm_normal/add/subtract/etc) to a GS ALPHA register value.
-static u64 gmsBlendModeToGSAlpha(int32_t mode) {
- switch (mode) {
- case bm_normal: return GS_SETREG_ALPHA(0, 1, 0, 1, 0); // (Cs-Cd)*As + Cd
- case bm_add: return GS_SETREG_ALPHA(0, 2, 0, 1, 0); // Cs*As + Cd
- case bm_subtract: return GS_SETREG_ALPHA(1, 0, 2, 2, 0x80); // Cd - Cs (clamped to 0)
- case bm_reverse_subtract: return GS_SETREG_ALPHA(2, 0, 0, 1, 0); // Cd - Cs*As
- case bm_min: return GS_SETREG_ALPHA(0, 1, 0, 1, 0); // No GS min, fall back to normal
- case bm_max: return GS_SETREG_ALPHA(0, 1, 0, 1, 0); // No GS max, fall back to normal
- default: return GS_SETREG_ALPHA(0, 1, 0, 1, 0);
- }
-}
-
-static void gsGpuSetBlendMode(Renderer* renderer, int32_t mode) {
- GsRenderer* gs = (GsRenderer*) renderer;
- gs->currentBlendAlpha = gmsBlendModeToGSAlpha(mode);
- gsCommitBlend(gs);
-}
-
-static void gsGpuSetBlendModeExt(Renderer* renderer, int32_t sfactor, int32_t dfactor) {
- GsRenderer* gs = (GsRenderer*) renderer;
- u64 alpha;
- if (!gmsFactorPairToGSAlpha(sfactor, dfactor, &alpha) && !gs->blendModeWarned) {
- fprintf(stderr, "GsRenderer: blend mode (sf=%d, df=%d) not exactly representable on PS2; approximating\n", sfactor, dfactor);
- gs->blendModeWarned = true;
- }
- gs->currentBlendAlpha = alpha;
- gsCommitBlend(gs);
-}
-
-static void gsGpuSetBlendEnable(Renderer* renderer, bool enable) {
- GsRenderer* gs = (GsRenderer*) renderer;
- // PrimAlphaEnable is OR'd into the PRIM bits gsKit emits with each primitive,
- // so toggling it affects every subsequent draw without needing to flush.
- if (gs->blendEnabled == enable) return;
- gs->blendEnabled = enable;
- gsCommitBlend(gs);
-}
-
-static void gsGpuSetAlphaTestEnable(Renderer* renderer, bool enable) {
- GsRenderer* gs = (GsRenderer*) renderer;
- GSGLOBAL* g = gs->gsGlobal;
- // Default to GREATER comparison when first enabling. If the ref hasn't been touched yet,
- // initialize to 0 so behavior matches GL (test passes when src_alpha > ref).
- if (enable) {
- g->Test->ATST = 6; // GREATER
- g->Test->AFAIL = 0; // KEEP (skip writing entirely)
- }
- gsKit_set_test(g, enable ? GS_ATEST_ON : GS_ATEST_OFF);
-}
-
-static void gsGpuSetAlphaTestRef(Renderer* renderer, uint8_t ref) {
- GsRenderer* gs = (GsRenderer*) renderer;
- GSGLOBAL* g = gs->gsGlobal;
- g->Test->AREF = alphaToGS(ref);
- g->Test->ATST = 6; // GREATER (matches GMS semantics: pass when src_alpha > ref)
- g->Test->AFAIL = 0; // KEEP
- // Preset 0 doesn't match any branch in gsKit_set_test, so it just re-emits TEST with current state.
- gsKit_set_test(g, 0);
-}
-
-static void gsGpuSetColorWriteEnable(Renderer* renderer, bool red, bool green, bool blue, bool alpha) {
- GsRenderer* gs = (GsRenderer*) renderer;
- // FBMSK: bit=1 means MASK that bit (don't write). Layout is the conceptual RGBA8888 mapping
- // even when the framebuffer is CT16 - the GS remaps the relevant bits internally.
- u32 fbmsk = 0;
- if (!red) fbmsk |= 0x000000FF;
- if (!green) fbmsk |= 0x0000FF00;
- if (!blue) fbmsk |= 0x00FF0000;
- if (!alpha) fbmsk |= 0xFF000000;
- gsApplyFBMask(gs, fbmsk);
-
- // Alpha-only write mode (color writes off, alpha on) is the dest-alpha mask write half: the script wants the source alpha to land in FB.A verbatim, so disable FBA.
- // Anything else keeps FBA=1 so normal blended sprites leave FB.A=1 and the mask read half (bm_dest_alpha / bm_inv_dest_alpha) sees opaque pixels.
- bool alphaOnly = !red && !green && !blue && alpha;
- gsApplyFBA(gs, alphaOnly ? 0 : 1);
-}
-
-static void gsDrawTile(Renderer* renderer, RoomTile* tile, float offsetX, float offsetY) {
- GsRenderer* gs = (GsRenderer*) renderer;
-
- // Look up the tile in the atlas tile entries
- AtlasTileEntry* tileEntry = findTileEntry(gs, (int16_t) tile->backgroundDefinition, (uint16_t) tile->sourceX, (uint16_t) tile->sourceY, (uint16_t) tile->width, (uint16_t) tile->height);
- if (tileEntry == nullptr)
- return;
-
- // Set up GSTEXTURE for this tile entry
- GSTEXTURE tex;
- if (!setupTextureForTile(gs, &tex, tileEntry))
- return;
-
- // Compute screen rect in game coordinates
- float drawX = (float) tile->x + offsetX;
- float drawY = (float) tile->y + offsetY;
- float drawW = (float) tile->width * tile->scaleX;
- float drawH = (float) tile->height * tile->scaleY;
-
- float sx1 = (drawX - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (drawY - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (drawX + drawW - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (drawY + drawH - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- // View frustum culling
- float minSX = (sx1 < sx2) ? sx1 : sx2;
- float maxSX = (sx1 > sx2) ? sx1 : sx2;
- float minSY = (sy1 < sy2) ? sy1 : sy2;
- float maxSY = (sy1 > sy2) ? sy1 : sy2;
- if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT)
- return;
-
- // UV coordinates in atlas texels
- float u1 = (float) tileEntry->atlasX;
- float v1 = (float) tileEntry->atlasY;
- float u2 = u1 + (float) tileEntry->width;
- float v2 = v1 + (float) tileEntry->height;
-
- // Extract alpha from tile color high byte, default to 1.0 if 0
- uint8_t alphaByte = (tile->color >> 24) & 0xFF;
- float alpha = (alphaByte == 0) ? 1.0f : (float) alphaByte / 255.0f;
- uint32_t bgr = tile->color & 0x00FFFFFF;
-
- // GS modulate mode: scale RGB from 0-255 to 0-128
- uint8_t r = BGR_R(bgr) >> 1;
- uint8_t g = BGR_G(bgr) >> 1;
- uint8_t b = BGR_B(bgr) >> 1;
- uint8_t a = alphaToGS(alpha);
- u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx1, sy1, u1, v1, sx2, sy2, u2, v2, 0, gsColor);
-}
-
-// ===[ Vtable ]===
-
-static RendererVtable gsVtable = {
- .init = gsInit,
- .destroy = gsDestroy,
- .beginFrame = gsBeginFrame,
- .endFrame = gsEndFrame,
- .beginView = gsBeginView,
- .endView = gsEndView,
- .beginGUI = gsBeginGUI,
- .endGUI = gsEndGUI,
- .drawSprite = gsDrawSprite,
- .drawSpritePos = gsDrawSpritePos,
- .drawSpritePart = gsDrawSpritePart,
- .drawRectangle = gsDrawRectangle,
- .drawLine = gsDrawLine,
- .drawLineColor = gsDrawLineColor,
- .drawText = gsDrawText,
- .drawTextColor = gsDrawTextColor,
- .drawTriangle = gsDrawTriangle,
- .flush = gsFlush,
- .createSpriteFromSurface = gsCreateSpriteFromSurface,
- .deleteSprite = gsDeleteSprite,
- .gpuSetBlendMode = gsGpuSetBlendMode,
- .gpuSetBlendModeExt = gsGpuSetBlendModeExt,
- .gpuSetBlendEnable = gsGpuSetBlendEnable,
- .gpuSetAlphaTestEnable = gsGpuSetAlphaTestEnable,
- .gpuSetAlphaTestRef = gsGpuSetAlphaTestRef,
- .gpuSetColorWriteEnable = gsGpuSetColorWriteEnable,
- .drawTile = gsDrawTile,
- .drawTiled = gsDrawTiled,
- .drawTiledPart = gsDrawTiledPart,
-};
-
-// ===[ Public API ]===
-
-Renderer* GsRenderer_create(GSGLOBAL* gsGlobal) {
- GsRenderer* gs = safeCalloc(1, sizeof(GsRenderer));
- gs->base.vtable = &gsVtable;
- gs->gsGlobal = gsGlobal;
- gs->scaleX = 2.0f;
- gs->scaleY = 2.0f;
- return (Renderer*) gs;
-}
diff --git a/src/ps2/gs_renderer.h b/src/ps2/gs_renderer.h
deleted file mode 100644
index dffeaa03..00000000
--- a/src/ps2/gs_renderer.h
+++ /dev/null
@@ -1,142 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "renderer.h"
-#include
-#include "stb_ds.h"
-
-// ===[ Atlas Entry (from ATLAS.BIN TPAG entries) ]===
-typedef struct {
- uint16_t atlasId; // TEX atlas index (0xFFFF = not mapped)
- uint16_t atlasX; // X offset within the atlas
- uint16_t atlasY; // Y offset within the atlas
- uint16_t width; // Image width in the atlas (post-crop, post-resize)
- uint16_t height; // Image height in the atlas (post-crop, post-resize)
- uint16_t cropX; // X offset of cropped content within original bounding box
- uint16_t cropY; // Y offset of cropped content within original bounding box
- uint16_t cropW; // Pre-resize width of the cropped content
- uint16_t cropH; // Pre-resize height of the cropped content
- uint16_t clutIndex; // CLUT index within the corresponding CLUT file
- uint8_t bpp; // 4 or 8
-} AtlasTPAGEntry;
-
-// ===[ Atlas Tile Entry (from ATLAS.BIN tile entries) ]===
-typedef struct {
- int16_t bgDef; // Background definition index
- uint16_t srcX; // Source X in the original background image
- uint16_t srcY; // Source Y in the original background image
- uint16_t srcW; // Original tile width in pixels
- uint16_t srcH; // Original tile height in pixels
- uint16_t atlasId; // TEX atlas index (0xFFFF = not mapped)
- uint16_t atlasX; // X offset within the atlas
- uint16_t atlasY; // Y offset within the atlas
- uint16_t width; // Tile width in the atlas (post-crop, post-resize)
- uint16_t height; // Tile height in the atlas (post-crop, post-resize)
- uint16_t cropX; // X offset of cropped content within original tile
- uint16_t cropY; // Y offset of cropped content within original tile
- uint16_t cropW; // Pre-resize width of the cropped content
- uint16_t cropH; // Pre-resize height of the cropped content
- uint16_t clutIndex; // CLUT index within the corresponding CLUT file
- uint8_t bpp; // 4 or 8
-} AtlasTileEntry;
-
-// ===[ Tile Lookup Key (for O(1) hashmap lookup) ]===
-typedef struct {
- int16_t bgDef;
- uint16_t srcX;
- uint16_t srcY;
- uint16_t srcW;
- uint16_t srcH;
-} TileLookupKey;
-
-// stb_ds hashmap entry: TileLookupKey -> AtlasTileEntry*
-typedef struct {
- TileLookupKey key;
- AtlasTileEntry* value;
-} TileEntryMap;
-
-// ===[ VRAM Chunk (buddy system unit) ]===
-// Each chunk is 128KB of VRAM (fits one 4bpp 512x512 atlas).
-// An 8bpp atlas uses 2 consecutive chunks.
-#define VRAM_CHUNK_SIZE 131072 // 128KB = gsKit_texture_size(512, 512, GS_PSM_T4)
-
-typedef struct {
- int16_t atlasId; // Which atlas occupies this chunk (-1 = free)
- uint64_t lastUsed; // Frame number when last accessed
-} VRAMChunk;
-
-// ===[ EE RAM Atlas Cache Entry ]===
-// Caches uncompressed atlas pixel data in EE RAM for zero-copy VRAM uploads to avoid repeated CDVD reads and decompression
-typedef struct {
- int16_t atlasId; // Which atlas (-1 = free)
- uint32_t offset; // Byte offset within eeCache buffer (128-byte aligned)
- uint32_t size; // Total bytes stored (uncompressed indexed pixels: 128KB for 4bpp, 256KB for 8bpp)
- uint64_t lastUsed; // Frame counter for LRU
-} EeAtlasCacheEntry;
-
-// ===[ GsRenderer Struct ]===
-typedef struct {
- Renderer base; // Must be first field for struct embedding
-
- GSGLOBAL* gsGlobal;
-
- // View transform state
- float scaleX;
- float scaleY;
- float offsetX;
- float offsetY;
- int32_t viewX;
- int32_t viewY;
-
- // ATLAS.BIN data
- uint16_t atlasTPAGCount;
- uint16_t atlasTileCount;
- AtlasTPAGEntry* atlasTPAGEntries;
- AtlasTileEntry* atlasTileEntries;
- TileEntryMap* tileEntryMap; // stb_ds hashmap: (bgDef, srcX, srcY, srcW, srcH) -> AtlasTileEntry*
-
- // CLUT VRAM addresses (one per CLUT, individually uploaded)
- uint32_t clut4Count; // Number of 4bpp CLUTs
- uint32_t* clut4VramAddrs; // Per-CLUT VRAM addresses [clut4Count]
-
- uint32_t clut8Count; // Number of 8bpp CLUTs
- uint32_t* clut8VramAddrs; // Per-CLUT VRAM addresses [clut8Count]
-
- // TEXTURES.BIN file handle (kept open for on-demand atlas loading)
- FILE* texturesFile;
- uint32_t* atlasOffsets; // Byte offset of each atlas within TEXTURES.BIN [atlasCount]
-
- // VRAM texture cache (buddy system with LRU eviction)
- uint32_t textureVramBase; // Start of texture region in VRAM (after framebuffers + CLUTs)
- uint32_t chunkCount; // Number of 128KB chunks available
- VRAMChunk* chunks; // Per-chunk state [chunkCount]
- int16_t* atlasToChunk; // atlasId -> first chunk index (-1 = not loaded) [atlasCount]
- uint16_t atlasCount; // Number of atlas IDs from ATLAS.BIN header
- uint8_t* atlasBpp; // Bits per pixel per atlas (4 or 8), from ATLAS.BIN [atlasCount]
- uint64_t frameCounter; // Incremented each frame for LRU tracking
- bool evictedAtlasUsedInCurrentFrame; // Used for debugging, true if a atlas that was used on the current frame was evicted (VRAM thrashing)
- uint16_t uniqueAtlasesThisFrame; // Number of distinct atlases touched this frame
- uint16_t chunksNeededThisFrame; // Total VRAM chunks needed by all atlases touched this frame
- uint16_t diskLoadsThisFrame; // Number of atlas loads from TEXTURES.BIN this frame (EE cache misses)
-
- // EE RAM atlas cache (stores uncompressed atlas pixel data for zero-copy VRAM uploads)
- uint8_t* eeCache; // Contiguous buffer with uncompressed texture data
- uint32_t eeCacheCapacity; // Total size (See EE_CACHE_CAPACITY)
- uint32_t eeCacheBumpPtr; // End of live data
- EeAtlasCacheEntry* eeCacheEntries; // Per-atlas cache state [atlasCount]
- uint32_t* atlasDataSizes; // On-disk size per atlas (header + compressed data) [atlasCount]
-
- // GPU state (mirrors what was last sent to GS so we can re-apply after sync_flip clobbers FRAME)
- uint32_t fbmsk; // Current FRAME register FBMSK (0 = all channels writable)
- uint8_t fba; // Current FBA_1 register value (1 = force FB.A bit to 1 on writeback, 0 = pass through)
- bool blendModeWarned; // Set the first time an unsupported blend factor pair is seen
-
- // gsKit packs PrimAlphaEnable into BOTH the PRIM.ABE bit AND TEX0.TCC.
- // So toggling ABE off also forces TCC=0, which makes the GS ignore the texture's per-pixel alpha and pull alpha from TA0 (default 0x00) instead.
- // That breaks textured alpha-mask sprites.
- // To keep TCC=1 always, we leave PrimAlphaEnable=ON and emulate blend-disable by switching ALPHA to an identity equation (Cs passes through unchanged).
- bool blendEnabled; // What the GML last requested via gpu_set_blendenable
- u64 currentBlendAlpha; // The ALPHA register value the GML last requested via gpu_set_blendmode[_ext]
-} GsRenderer;
-
-Renderer* GsRenderer_create(GSGLOBAL* gsGlobal);
diff --git a/src/ps2/gs_renderer_flat.c b/src/ps2/gs_renderer_flat.c
deleted file mode 100644
index ec3b91c0..00000000
--- a/src/ps2/gs_renderer_flat.c
+++ /dev/null
@@ -1,342 +0,0 @@
-#include "gs_renderer_flat.h"
-
-#include
-#include
-#include
-
-#include "utils.h"
-#include "text_utils.h"
-#include "ps2_utils.h"
-
-// ===[ Color Generation ]===
-// Generates a unique color for each tpagIndex so sprites are visually distinguishable.
-// Uses a simple hash to spread colors across the RGB space.
-static u64 colorForTpagIndex(int32_t tpagIndex, float alpha) {
- // Golden ratio hash for good color distribution
- uint32_t hash = (uint32_t) tpagIndex * 2654435761u;
- uint8_t r = (uint8_t) ((hash >> 0) & 0xFF);
- uint8_t g = (uint8_t) ((hash >> 8) & 0xFF);
- uint8_t b = (uint8_t) ((hash >> 16) & 0xFF);
-
- // Ensure colors are never too dark (min brightness ~100)
- if (128 > r + g + b) {
- r = (uint8_t) (r | 0x80);
- g = (uint8_t) (g | 0x40);
- }
-
- uint8_t a = alphaToGS(alpha);
- return GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-}
-
-// ===[ Vtable Implementations ]===
-
-static void gsInit(Renderer* renderer, DataWin* dataWin) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
-
- renderer->dataWin = dataWin;
- renderer->drawColor = 0xFFFFFF;
- renderer->drawAlpha = 1.0f;
- renderer->drawFont = -1;
- renderer->drawHalign = 0;
- renderer->drawValign = 0;
-
- // Enable alpha blending on all primitives (sets ABE bit in GS PRIM register)
- gs->gsGlobal->PrimAlphaEnable = GS_SETTING_ON;
-
- // Set alpha blend equation: (Cs - Cd) * As / 128 + Cd (standard src-over blend)
- gsKit_set_primalpha(gs->gsGlobal, GS_SETREG_ALPHA(0, 1, 0, 1, 0), 0);
-
- printf("GsRendererFlat: initialized (colored quads mode, no textures)\n");
- printf("GsRendererFlat: %u sprites, %u TPAG items\n", dataWin->sprt.count, dataWin->tpag.count);
-}
-
-static void gsDestroy(Renderer* renderer) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
- free(gs);
-}
-
-static void gsBeginFrame(Renderer* renderer, MAYBE_UNUSED int32_t gameW, MAYBE_UNUSED int32_t gameH, MAYBE_UNUSED int32_t windowW, MAYBE_UNUSED int32_t windowH) {}
-
-static void gsEndFrame(MAYBE_UNUSED Renderer* renderer) {
- // No-op: flip happens in main loop
-}
-
-static void gsBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, int32_t portW, int32_t portH, MAYBE_UNUSED float viewAngle) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
- gs->viewX = viewX;
- gs->viewY = viewY;
-
- // Scale game view to PS2 screen (640x448 NTSC interlaced)
- // Use uniform scale based on width (640/viewW) so pixels stay square.
- if (viewW > 0 && viewH > 0) {
- gs->scaleX = 640.0f / (float) viewW;
- gs->scaleY = gs->scaleX;
- } else {
- gs->scaleX = 2.0f;
- gs->scaleY = 2.0f;
- }
-
- // Center vertically: offset so the rendered image is centered on the 448px screen
- float renderedH = (float) viewH * gs->scaleY;
- gs->offsetX = 0.0f;
- gs->offsetY = (448.0f - renderedH) / 2.0f;
-}
-
-static void gsEndView(MAYBE_UNUSED Renderer* renderer) {
- // No-op
-}
-
-static void gsBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, MAYBE_UNUSED int32_t portW, MAYBE_UNUSED int32_t portH) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
- gs->viewX = 0;
- gs->viewY = 0;
-
- if (guiW > 0 && guiH > 0) {
- gs->scaleX = 640.0f / (float) guiW;
- gs->scaleY = gs->scaleX;
- } else {
- gs->scaleX = 2.0f;
- gs->scaleY = 2.0f;
- }
-
- float renderedH = (float) guiH * gs->scaleY;
- gs->offsetX = 0.0f;
- gs->offsetY = (448.0f - renderedH) / 2.0f;
-}
-
-static void gsEndGUI(MAYBE_UNUSED Renderer* renderer) {
- // No-op
-}
-
-static void gsDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, MAYBE_UNUSED float angleDeg, MAYBE_UNUSED uint32_t color, float alpha) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return;
-
- TexturePageItem* tpag = &dw->tpag.items[tpagIndex];
- float w = (float) tpag->boundingWidth;
- float h = (float) tpag->boundingHeight;
-
- // Compute screen rect in game coordinates
- float gameX1 = x - originX * xscale;
- float gameY1 = y - originY * yscale;
- float gameX2 = x + (w - originX) * xscale;
- float gameY2 = y + (h - originY) * yscale;
-
- // Apply view offset
- gameX1 -= (float) gs->viewX;
- gameY1 -= (float) gs->viewY;
- gameX2 -= (float) gs->viewX;
- gameY2 -= (float) gs->viewY;
-
- // Scale to screen coordinates
- float sx1 = gameX1 * gs->scaleX + gs->offsetX;
- float sy1 = gameY1 * gs->scaleY + gs->offsetY;
- float sx2 = gameX2 * gs->scaleX + gs->offsetX;
- float sy2 = gameY2 * gs->scaleY + gs->offsetY;
-
- u64 quadColor = colorForTpagIndex(tpagIndex, alpha);
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, quadColor);
-}
-
-static void gsDrawSpritePart(Renderer* renderer, int32_t tpagIndex, MAYBE_UNUSED int32_t srcOffX, MAYBE_UNUSED int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg, MAYBE_UNUSED float pivotX, MAYBE_UNUSED float pivotY, MAYBE_UNUSED uint32_t color, float alpha) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
-
- if (0 > tpagIndex || (uint32_t) tpagIndex >= renderer->dataWin->tpag.count) return;
-
- // Compute screen rect
- float gameX1 = x - (float) gs->viewX;
- float gameY1 = y - (float) gs->viewY;
- float gameX2 = gameX1 + (float) srcW * xscale;
- float gameY2 = gameY1 + (float) srcH * yscale;
-
- float sx1 = gameX1 * gs->scaleX + gs->offsetX;
- float sy1 = gameY1 * gs->scaleY + gs->offsetY;
- float sx2 = gameX2 * gs->scaleX + gs->offsetX;
- float sy2 = gameY2 * gs->scaleY + gs->offsetY;
-
- u64 quadColor = colorForTpagIndex(tpagIndex, alpha);
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, quadColor);
-}
-
-static void gsDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, MAYBE_UNUSED bool outline) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
-
- // BGR to RGB
- uint8_t r = BGR_R(color);
- uint8_t g = BGR_G(color);
- uint8_t b = BGR_B(color);
- uint8_t a = alphaToGS(alpha);
-
- float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- u64 rectColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, rectColor);
-}
-
-static void gsDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, MAYBE_UNUSED float width, uint32_t color, float alpha) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
-
- uint8_t r = BGR_R(color);
- uint8_t g = BGR_G(color);
- uint8_t b = BGR_B(color);
- uint8_t a = alphaToGS(alpha);
-
- float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- u64 lineColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
- gsKit_prim_line(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, lineColor);
-}
-
-// PS2 gsKit doesn't support per-vertex colors on lines, so we just use color1
-static void gsDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, MAYBE_UNUSED uint32_t color2, float alpha) {
- renderer->vtable->drawLine(renderer, x1, y1, x2, y2, width, color1, alpha);
-}
-
-static void gsDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg) {
- GsRendererFlat* gs = (GsRendererFlat*) renderer;
- DataWin* dw = renderer->dataWin;
-
- if (0 > renderer->drawFont || (uint32_t) renderer->drawFont >= dw->font.count) return;
-
- Font* font = &dw->font.fonts[renderer->drawFont];
-
- // BGR to RGB for text color
- uint32_t color = renderer->drawColor;
- uint8_t r = BGR_R(color);
- uint8_t g = BGR_G(color);
- uint8_t b = BGR_B(color);
- uint8_t a = alphaToGS(renderer->drawAlpha);
- u64 textColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00);
-
- int32_t textLen = (int32_t) strlen(text);
-
- // Compute vertical alignment offset
- int32_t lineCount = TextUtils_countLines(text, textLen);
- float lineStride = TextUtils_lineStride(font);
- float totalHeight = (float) lineCount * lineStride;
- float valignOffset = 0;
- if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f;
- else if (renderer->drawValign == 2) valignOffset = -totalHeight;
-
- float cursorY = valignOffset - (float) font->ascenderOffset;
- int32_t lineStart = 0;
-
- while (textLen >= lineStart) {
- // Find end of current line
- int32_t lineEnd = lineStart;
- while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) {
- lineEnd++;
- }
-
- int32_t lineLen = lineEnd - lineStart;
- const char* line = text + lineStart;
-
- // Measure line width for horizontal alignment
- float lineWidth = TextUtils_measureLineWidth(font, line, lineLen);
- float halignOffset = 0;
- if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f;
- else if (renderer->drawHalign == 2) halignOffset = -lineWidth;
-
- float cursorX = halignOffset;
-
- // Draw each glyph as a colored rectangle - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning)
- int32_t pos = 0;
- uint16_t ch = 0;
- bool hasCh = false;
- if (lineLen > pos) {
- ch = TextUtils_decodeUtf8(line, lineLen, &pos);
- hasCh = true;
- }
-
- while (hasCh) {
- FontGlyph* glyph = TextUtils_findGlyph(font, ch);
-
- uint16_t nextCh = 0;
- bool hasNext = lineLen > pos;
- if (hasNext) nextCh = TextUtils_decodeUtf8(line, lineLen, &pos);
-
- if (glyph != nullptr) {
- if (glyph->sourceWidth > 0 && glyph->sourceHeight > 0) {
- float glyphX = x + (cursorX + (float) glyph->offset) * xscale * font->scaleX;
- float glyphY = y + cursorY * yscale * font->scaleY;
- float glyphW = (float) glyph->sourceWidth * xscale * font->scaleX;
- float glyphH = (float) glyph->sourceHeight * yscale * font->scaleY;
-
- // Apply view offset and scale to screen coordinates
- float sx1 = (glyphX - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy1 = (glyphY - (float) gs->viewY) * gs->scaleY + gs->offsetY;
- float sx2 = (glyphX + glyphW - (float) gs->viewX) * gs->scaleX + gs->offsetX;
- float sy2 = (glyphY + glyphH - (float) gs->viewY) * gs->scaleY + gs->offsetY;
-
- gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, textColor);
- }
-
- cursorX += (float) glyph->shift;
- if (hasNext) cursorX += TextUtils_getKerningOffset(glyph, nextCh);
- }
-
- ch = nextCh;
- hasCh = hasNext;
- }
-
- // Advance to next line
- cursorY += lineStride;
- if (textLen > lineEnd) {
- lineStart = TextUtils_skipNewline(text, lineEnd, textLen);
- } else {
- break;
- }
- }
-}
-
-static void gsFlush(MAYBE_UNUSED Renderer* renderer) {
- // No-op: gsKit queues commands, executed in main loop via gsKit_queue_exec
-}
-
-static int32_t gsCreateSpriteFromSurface(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t x, MAYBE_UNUSED int32_t y, MAYBE_UNUSED int32_t w, MAYBE_UNUSED int32_t h, MAYBE_UNUSED bool removeback, MAYBE_UNUSED bool smooth, MAYBE_UNUSED int32_t xorig, MAYBE_UNUSED int32_t yorig) {
- fprintf(stderr, "GsRendererFlat: createSpriteFromSurface not supported on PS2\n");
- return -1;
-}
-
-static void gsDeleteSprite(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t spriteIndex) {
- // No-op
-}
-
-// ===[ Constructor ]===
-
-static RendererVtable gsVtable = {
- .init = gsInit,
- .destroy = gsDestroy,
- .beginFrame = gsBeginFrame,
- .endFrame = gsEndFrame,
- .beginView = gsBeginView,
- .endView = gsEndView,
- .beginGUI = gsBeginGUI,
- .endGUI = gsEndGUI,
- .drawSprite = gsDrawSprite,
- .drawSpritePart = gsDrawSpritePart,
- .drawRectangle = gsDrawRectangle,
- .drawLine = gsDrawLine,
- .drawLineColor = gsDrawLineColor,
- .drawText = gsDrawText,
- .flush = gsFlush,
- .createSpriteFromSurface = gsCreateSpriteFromSurface,
- .deleteSprite = gsDeleteSprite,
-};
-
-Renderer* GsRendererFlat_create(GSGLOBAL* gsGlobal) {
- GsRendererFlat* gs = safeCalloc(1, sizeof(GsRendererFlat));
- gs->base.vtable = &gsVtable;
- gs->gsGlobal = gsGlobal;
- gs->scaleX = 2.0f;
- gs->scaleY = 2.0f;
- return (Renderer*) gs;
-}
diff --git a/src/ps2/gs_renderer_flat.h b/src/ps2/gs_renderer_flat.h
deleted file mode 100644
index 4f49b41d..00000000
--- a/src/ps2/gs_renderer_flat.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "renderer.h"
-#include
-
-// ===[ GsRendererFlat Struct ]===
-// Simple PS2 renderer using gsKit ONE SHOT mode.
-// Renders all sprites/text as colored rectangles (no textures).
-typedef struct {
- Renderer base; // Must be first field for struct embedding
-
- GSGLOBAL* gsGlobal;
-
- // View transform state (set each view in beginView)
- float scaleX;
- float scaleY;
- float offsetX;
- float offsetY;
- int32_t viewX;
- int32_t viewY;
-} GsRendererFlat;
-
-Renderer* GsRendererFlat_create(GSGLOBAL* gsGlobal);
diff --git a/src/ps2/main.c b/src/ps2/main.c
deleted file mode 100644
index cb101601..00000000
--- a/src/ps2/main.c
+++ /dev/null
@@ -1,942 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "runner.h"
-#include "runner_keyboard.h"
-#include "vm.h"
-#include "../data_win.h"
-#include "../json_reader.h"
-#include "ps2_file_system.h"
-#ifdef ENABLE_PS2_AUDIO
-#include "ps2_audio_system.h"
-#endif
-#include "gs_renderer.h"
-#include "noop_audio_system.h"
-#include "ps2_utils.h"
-#include "stb_ds.h"
-#include "utils.h"
-#include "../profiler.h"
-
-#ifdef GPROF_PROFILING
-#include
-#endif
-
-// Embedded ps2sdk IRX modules (generated by bin2c at build time)
-extern unsigned char freesio2_irx[];
-extern unsigned int size_freesio2_irx;
-extern unsigned char mcman_irx[];
-extern unsigned int size_mcman_irx;
-extern unsigned char mcserv_irx[];
-extern unsigned int size_mcserv_irx;
-extern unsigned char freepad_irx[];
-extern unsigned int size_freepad_irx;
-extern unsigned char usbd_irx[];
-extern unsigned int size_usbd_irx;
-extern unsigned char ps2kbd_irx[];
-extern unsigned int size_ps2kbd_irx;
-#ifdef ENABLE_PS2_AUDIO
-extern unsigned char freesd_irx[];
-extern unsigned int size_freesd_irx;
-extern unsigned char audsrv_irx[];
-extern unsigned int size_audsrv_irx;
-#endif
-
-// Total main RAM in bytes.
-static int MAX_MEMORY_BYTES = 0;
-
-// Heap ceiling: Captured once at startup (before any game allocations) and used as a stable denominator in the debug overlay.
-static int heapCeilingBytes = 0;
-
-// 256-byte aligned buffers for libpad (one per port)
-static char padBuf[2][256] __attribute__((aligned(64)));
-
-// Controller button to GML key mapping
-typedef struct {
- uint16_t padButton;
- int32_t gmlKey;
-} PadMapping;
-
-static PadMapping* pad1Mappings = nullptr;
-static int pad1MappingCount = 0;
-static PadMapping* pad2Mappings = nullptr;
-static int pad2MappingCount = 0;
-
-// Previous frame's button state per pad (active-low; 0xFFFF = all released)
-static uint16_t prevButtons[2] = {0xFFFF, 0xFFFF};
-
-// Whether each port was successfully opened (padPortOpen succeeded)
-static bool padOpened[2] = {false, false};
-
-// Whether each pad was STABLE last frame (for disconnect/reconnect edge detection)
-static bool padWasStable[2] = {false, false};
-
-static void parsePadMappings(JsonValue* configRoot, const char* key, PadMapping** outMappings, int* outCount, const char* logLabel) {
- JsonValue* mappingsObj = JsonReader_getObject(configRoot, key);
- if (mappingsObj == nullptr || !JsonReader_isObject(mappingsObj)) return;
- int count = JsonReader_objectLength(mappingsObj);
- PadMapping* mappings = safeMalloc(sizeof(PadMapping) * count);
- repeat(count, i) {
- const char* padButtonStr = JsonReader_getObjectKey(mappingsObj, i);
- JsonValue* gmlKeyVal = JsonReader_getObjectValue(mappingsObj, i);
- mappings[i].padButton = (uint16_t) atoi(padButtonStr);
- mappings[i].gmlKey = (int32_t) JsonReader_getInt(gmlKeyVal);
- printf("CONFIG.JSN: %s mapping pad=%d -> gmlKey=%d\n", logLabel, mappings[i].padButton, mappings[i].gmlKey);
- }
- *outMappings = mappings;
- *outCount = count;
-}
-
-static void pollPad(Runner* runner, int port, PadMapping* mappings, int mappingCount, uint16_t* prev, bool* wasStable) {
- int state = padGetState(port, 0);
- bool stable = (state == PAD_STATE_STABLE);
-
- if (!stable) {
- if (*wasStable) {
- // Disconnect edge: release every key whose button was held.
- repeat(mappingCount, i) {
- uint16_t mask = mappings[i].padButton;
- if ((*prev & mask) == 0) {
- RunnerKeyboard_onKeyUp(runner->keyboard, mappings[i].gmlKey);
- }
- }
- *prev = 0xFFFF;
- *wasStable = false;
- }
- return;
- }
-
- if (!*wasStable) {
- // Reconnect edge: avoid phantom presses from buttons that were held when the pad came back.
- *prev = 0xFFFF;
- *wasStable = true;
- }
-
- struct padButtonStatus padStatus;
- unsigned char padResult = padRead(port, 0, &padStatus);
- if (padResult == 0) return;
-
- uint16_t buttons = padStatus.btns;
- repeat(mappingCount, i) {
- uint16_t mask = mappings[i].padButton;
- int32_t gmlKey = mappings[i].gmlKey;
-
- // PS2 buttons are active-low: 0 = pressed, 1 = released
- bool wasPressed = (*prev & mask) == 0;
- bool isPressed = (buttons & mask) == 0;
-
- if (isPressed && !wasPressed) {
- RunnerKeyboard_onKeyDown(runner->keyboard, gmlKey);
- } else if (!isPressed && wasPressed) {
- RunnerKeyboard_onKeyUp(runner->keyboard, gmlKey);
- }
- }
- *prev = buttons;
-}
-
-// ===[ USB Keyboard ]===
-
-static bool kbdAvailable = false;
-
-// Shift modifier state (left-or-right).
-static bool kbdShiftHeld = false;
-
-// Map a USB HID usage code (as delivered by ps2kbd.irx in RAW mode) to a GML VK code.
-static int32_t hidUsageToGmlKey(uint8_t hid) {
- // Letters: HID 0x04..0x1D -> ASCII 'A'..'Z' (GML uses uppercase ASCII)
- if (hid >= 0x04 && hid <= 0x1D) return (int32_t) ('A' + (hid - 0x04));
- // Numbers: HID 0x1E..0x26 -> '1'..'9', 0x27 -> '0'
- if (hid >= 0x1E && hid <= 0x26) return (int32_t) ('1' + (hid - 0x1E));
- if (hid == 0x27) return (int32_t) '0';
- // Special keys need mapping
- switch (hid) {
- case 0x28: return VK_ENTER; // Enter
- case 0x29: return VK_ESCAPE; // Escape
- case 0x2A: return VK_BACKSPACE; // Backspace
- case 0x2B: return VK_TAB; // Tab
- case 0x2C: return VK_SPACE; // Space
- case 0x3A: return VK_F1;
- case 0x3B: return VK_F2;
- case 0x3C: return VK_F3;
- case 0x3D: return VK_F4;
- case 0x3E: return VK_F5;
- case 0x3F: return VK_F6;
- case 0x40: return VK_F7;
- case 0x41: return VK_F8;
- case 0x42: return VK_F9;
- case 0x43: return VK_F10;
- case 0x44: return VK_F11;
- case 0x45: return VK_F12;
- case 0x49: return VK_INSERT;
- case 0x4A: return VK_HOME;
- case 0x4B: return VK_PAGEUP;
- case 0x4C: return VK_DELETE;
- case 0x4D: return VK_END;
- case 0x4E: return VK_PAGEDOWN;
- case 0x4F: return VK_RIGHT;
- case 0x50: return VK_LEFT;
- case 0x51: return VK_DOWN;
- case 0x52: return VK_UP;
- case 0xE0: case 0xE4: return VK_CONTROL; // Left/Right Ctrl
- case 0xE1: case 0xE5: return VK_SHIFT; // Left/Right Shift
- case 0xE2: case 0xE6: return VK_ALT; // Left/Right Alt
- default: return -1;
- }
-}
-
-// Translate a HID usage code to an ASCII character for RunnerKeyboard_onCharacter.
-// Also handles when the shift key is held.
-static unsigned int hidUsageToAsciiChar(uint8_t hid, bool shift) {
- // Letters A-Z: HID 0x04..0x1D
- if (hid >= 0x04 && hid <= 0x1D) {
- char base = (char) ('a' + (hid - 0x04));
- return (unsigned int) (shift ? (base - 32) : base);
- }
-
- // Digits / top-row symbols: 0x1E..0x26 -> 1..9, 0x27 -> 0
- static const char digitsUnshifted[10] = {'1','2','3','4','5','6','7','8','9','0'};
- static const char digitsShifted[10] = {'!','@','#','$','%','^','&','*','(',')'};
- if (hid >= 0x1E && hid <= 0x26) return (unsigned int) (shift ? digitsShifted[hid - 0x1E] : digitsUnshifted[hid - 0x1E]);
- if (hid == 0x27) return (unsigned int) (shift ? digitsShifted[9] : digitsUnshifted[9]);
-
- switch (hid) {
- case 0x28: return (unsigned int) '\r'; // Enter
- case 0x2A: return (unsigned int) '\b'; // Backspace
- case 0x2B: return (unsigned int) '\t'; // Tab
- case 0x2C: return (unsigned int) ' '; // Space
- case 0x2D: return (unsigned int) (shift ? '_' : '-');
- case 0x2E: return (unsigned int) (shift ? '+' : '=');
- case 0x2F: return (unsigned int) (shift ? '{' : '[');
- case 0x30: return (unsigned int) (shift ? '}' : ']');
- case 0x31: return (unsigned int) (shift ? '|' : '\\');
- case 0x33: return (unsigned int) (shift ? ':' : ';');
- case 0x34: return (unsigned int) (shift ? '"' : '\'');
- case 0x35: return (unsigned int) (shift ? '~' : '`');
- case 0x36: return (unsigned int) (shift ? '<' : ',');
- case 0x37: return (unsigned int) (shift ? '>' : '.');
- case 0x38: return (unsigned int) (shift ? '?' : '/');
- default: return 0;
- }
-}
-
-// ===[ Loading Screen ]===
-
-// Maximum number of chunk stats we track (24 chunks in data.win, but only some have interesting counts)
-#define MAX_CHUNK_STATS 24
-
-typedef struct {
- char label[16];
- uint32_t count;
-} ChunkStat;
-
-typedef struct {
- GSGLOBAL* gsGlobal;
- GSFONTM* gsFontM;
- ChunkStat stats[MAX_CHUNK_STATS];
- int statCount;
-} LoadingScreenState;
-
-// Draws the bottom-left credits text (shared between status screen and loading screen)
-static void drawCreditsText(GSGLOBAL* gs, GSFONTM* fontm) {
- u64 darkGray = GS_SETREG_RGBAQ(0x70, 0x70, 0x70, 0x80, 0x00);
- float creditsScale = 0.4f;
- float lineHeight = 26.0f * creditsScale;
- float creditsY = 448.0f - 10.0f - lineHeight * 2.0f;
-
- char versionText[128];
- snprintf(versionText, sizeof(versionText), "Butterscotch (%s) [%s]", BUTTERSCOTCH_COMMIT_HASH, BUTTERSCOTCH_COMMIT_DATE);
- gsKit_fontm_print_scaled(gs, fontm, 10.0f, creditsY, 1, creditsScale, darkGray, versionText);
- gsKit_fontm_print_scaled(gs, fontm, 10.0f, creditsY + lineHeight, 1, creditsScale, darkGray, "Created by MrPowerGamerBR (https://mrpowergamerbr.com/)");
-}
-
-// Draws a simple status screen with "Butterscotch" title, optional game name, and a status message (no progress bar)
-// gameName can be nullptr if the game name is not yet known
-// Begins a status screen: clears, draws title + optional game name, leaves center align active
-static void beginStatusScreen(GSGLOBAL* gs, GSFONTM* fontm, const char* gameName) {
- gsKit_clear(gs, GS_SETREG_RGBAQ(0x00, 0x00, 0x00, 0x80, 0x00));
-
- u64 title = GS_SETREG_RGBAQ(0x5E, 0x54, 0x92, 0x80, 0x00);
- u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00);
-
- fontm->Align = GSKIT_FALIGN_CENTER;
- gsKit_fontm_print_scaled(gs, fontm, 320.0f, 180.0f, 1, 0.8f, title, "Butterscotch");
- if (gameName) {
- gsKit_fontm_print_scaled(gs, fontm, 320.0f, 210.0f, 1, 0.5f, gray, gameName);
- }
-}
-
-// Ends a status screen: draws credits, resets align, flips
-static void endStatusScreen(GSGLOBAL* gs, GSFONTM* fontm) {
- fontm->Align = GSKIT_FALIGN_LEFT;
- drawCreditsText(gs, fontm);
- gsKit_queue_exec(gs);
- gsKit_sync_flip(gs);
-}
-
-// Draws chunk item counts in the top-left corner (if any stats have been recorded)
-static void drawChunkStats(GSGLOBAL* gs, GSFONTM* fontm, LoadingScreenState* loadingState) {
- if (!loadingState || loadingState->statCount == 0)
- return;
-
- u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00);
- fontm->Align = GSKIT_FALIGN_LEFT;
- float statsY = 10.0f;
- float statsScale = 0.35f;
- float statsLineHeight = 14.0f;
- char statLine[32];
-
- repeat(loadingState->statCount, i) {
- snprintf(statLine, sizeof(statLine), "%d %s", loadingState->stats[i].count, loadingState->stats[i].label);
- gsKit_fontm_print_scaled(gs, fontm, 10.0f, statsY, 1, statsScale, gray, statLine);
- statsY += statsLineHeight;
- }
-}
-
-static void drawStatusScreen(GSGLOBAL* gs, GSFONTM* fontm, const char* gameName, const char* statusText, LoadingScreenState* loadingState) {
- beginStatusScreen(gs, fontm, gameName);
- u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00);
- gsKit_fontm_print_scaled(gs, fontm, 320.0f, 300.0f, 1, 0.5f, gray, statusText);
- drawChunkStats(gs, fontm, loadingState);
- endStatusScreen(gs, fontm);
-}
-
-static void loadingScreenCallback(const char* chunkName, int chunkIndex, int totalChunks, DataWin* dataWin, void* userData) {
- LoadingScreenState* state = (LoadingScreenState*) userData;
- GSGLOBAL* gs = state->gsGlobal;
- GSFONTM* fontm = state->gsFontM;
-
- const char* gameName = dataWin->gen8.displayName ? dataWin->gen8.displayName : "Unknown Game";
- beginStatusScreen(gs, fontm, gameName);
-
- // Loading bar
- u64 white = GS_SETREG_RGBAQ(0xFF, 0xFF, 0xFF, 0x80, 0x00);
- u64 barBg = GS_SETREG_RGBAQ(0x40, 0x40, 0x40, 0x80, 0x00);
- u64 barFg = GS_SETREG_RGBAQ(0xFF, 0xCC, 0x00, 0x80, 0x00); // Butterscotch yellow
-
- float barX = 120.0f;
- float barY = 300.0f;
- float barW = 400.0f;
- float barH = 20.0f;
- float progress = (float) (chunkIndex + 1) / (float) totalChunks;
-
- // Bar background (dark gray)
- gsKit_prim_sprite(gs, barX, barY, barX + barW, barY + barH, 1, barBg);
-
- // Bar fill (butterscotch yellow)
- float fillW = barW * progress;
- if (fillW > 1.0f) {
- gsKit_prim_sprite(gs, barX, barY, barX + fillW, barY + barH, 1, barFg);
- }
-
- // Enable alpha blending so the font text doesn't have a black box behind it
- gs->PrimAlphaEnable = GS_SETTING_ON;
- gsKit_set_primalpha(gs, GS_SETREG_ALPHA(0, 1, 0, 1, 0), 0);
-
- // Percentage text centered on the bar
- char percentText[8];
- snprintf(percentText, sizeof(percentText), "%d%%", (int) (progress * 100));
- gsKit_fontm_print_scaled(gs, fontm, 320.0f, barY + 4.5f, 1, 0.4f, white, percentText);
-
- // Chunk name text below the bar
- char statusText[32];
- snprintf(statusText, sizeof(statusText), "Loading %.4s... (%d/%d)", chunkName, chunkIndex + 1, totalChunks);
- gsKit_fontm_print_scaled(gs, fontm, 320.0f, barY + barH + 10.0f, 1, 0.5f, white, statusText);
-
- // Memory usage below the status text
- u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00);
- void* heapTop = sbrk(0);
- int32_t usedBytes = (int32_t) (uintptr_t) heapTop;
- char memText[48];
- snprintf(memText, sizeof(memText), "Memory: %.1f/%.1f MB", (double) (usedBytes / (1024.0f * 1024.0f)), (double) (MAX_MEMORY_BYTES / (1024.0f * 1024.0f)));
- gsKit_fontm_print_scaled(gs, fontm, 320.0f, barY + barH + 30.0f, 1, 0.4f, gray, memText);
-
- // Record item counts for already-parsed chunks (callback fires before parsing, so we scan all counts each time and add any newly non-zero ones in the order they appear)
- typedef struct { uint32_t* countPtr; const char* label; } CountSource;
- CountSource sources[] = {
- { &dataWin->sond.count, "sounds" },
- { &dataWin->sprt.count, "sprites" },
- { &dataWin->bgnd.count, "backgrounds" },
- { &dataWin->font.count, "fonts" },
- { &dataWin->objt.count, "objects" },
- { &dataWin->room.count, "rooms" },
- { &dataWin->code.count, "code entries" },
- { &dataWin->txtr.count, "textures" },
- };
-
- // sizeof(sources) = size of the ENTIRE array
- // So, if we divide the size of the ENTIRE array by the size of a SINGLE entry, we get the number of entries
- int arrayLength = sizeof(sources) / sizeof(CountSource);
-
- repeat(arrayLength, i) {
- if (*sources[i].countPtr == 0)
- continue;
-
- // Check if we already recorded this label
- bool found = false;
- forEach(CountSource, stat, sources, state->statCount) {
- if (strcmp(stat->label, sources[i].label) == 0) {
- found = true;
- break;
- }
- }
-
- if (!found && MAX_CHUNK_STATS > state->statCount) {
- ChunkStat* stat = &state->stats[state->statCount++];
- snprintf(stat->label, sizeof(stat->label), "%s", sources[i].label);
- stat->count = *sources[i].countPtr;
- }
- }
-
- drawChunkStats(gs, fontm, state);
-
- gs->PrimAlphaEnable = GS_SETTING_OFF;
-
- endStatusScreen(gs, fontm);
-}
-
-int main(int argc, char* argv[]) {
- SifInitRpc(0);
- sbv_patch_enable_lmb();
-
- // Ask the kernel how much main RAM we actually have.
- MAX_MEMORY_BYTES = (int) GetMemorySize();
-
- // Snapshot the heap ceiling BEFORE anything else allocates. sbrk(0) at this point is the heap frontier after newlib's baseline reservations; mallinfo.uordblks is what newlib has already handed out to startup code. Their sum (added to the sbrk runway) is the total bytes user code could ever cumulatively hold live.
- {
- struct mallinfo mi = mallinfo();
- heapCeilingBytes = (MAX_MEMORY_BYTES - (int) (uintptr_t) sbrk(0)) + mi.uordblks;
- }
-
- PS2Utils_extractDeviceKey(argv[0]);
-
- fprintf(stderr, "argv0 is %s, device key is %s\n", argv[0], deviceKey.key);
-
- PS2Utils_loadFSDrivers();
-
- fprintf(stderr, "Loaded FS drivers!\n");
-
- const char* dataWinPath = PS2Utils_createDevicePath("DATA.WIN");
-
- printf("Butterscotch PS2 - Loading %s\n", dataWinPath);
-
- // ===[ Initialize gsKit ]===
- // This must happen first so we can show the loading screen during other init steps
- GSGLOBAL* gsGlobal = gsKit_init_global();
- gsGlobal->Mode = GS_MODE_NTSC;
- gsGlobal->Interlace = GS_INTERLACED;
- gsGlobal->Field = GS_FIELD;
- gsGlobal->Width = 640;
- gsGlobal->Height = 448;
- gsGlobal->PSM = GS_PSM_CT16;
- gsGlobal->PSMZ = GS_PSMZ_16;
- gsGlobal->DoubleBuffering = GS_SETTING_ON;
- gsGlobal->ZBuffering = GS_SETTING_OFF;
-
- gsGlobal->PrimAAEnable = GS_SETTING_OFF;
-
- dmaKit_init(D_CTRL_RELE_OFF, D_CTRL_MFD_OFF, D_CTRL_STS_UNSPEC, D_CTRL_STD_OFF, D_CTRL_RCYC_8, 1 << DMA_CHANNEL_GIF);
- dmaKit_chan_init(DMA_CHANNEL_GIF);
-
- gsKit_init_screen(gsGlobal);
- // Use ONE SHOT mode
- gsKit_mode_switch(gsGlobal, GS_ONESHOT);
-
- // ===[ Initialize FONTM (ROM font) for debug overlay ]===
- GSFONTM* gsFontM = gsKit_init_fontm();
- gsKit_fontm_upload(gsGlobal, gsFontM);
- gsFontM->Spacing = 0.95f;
-
- // ===[ Initialize Controller ]===
- drawStatusScreen(gsGlobal, gsFontM, nullptr, "Initializing controller...", nullptr);
-
- int ret;
- ret = SifExecModuleBuffer(freesio2_irx, size_freesio2_irx, 0, nullptr, nullptr);
- if (0 > ret) {
- printf("Failed to load freesio2: %d\n", ret);
- return 1;
- }
- ret = SifExecModuleBuffer(mcman_irx, size_mcman_irx, 0, nullptr, nullptr);
- if (0 > ret) {
- printf("Failed to load mcman: %d\n", ret);
- return 1;
- }
- ret = SifExecModuleBuffer(mcserv_irx, size_mcserv_irx, 0, nullptr, nullptr);
- if (0 > ret) {
- printf("Failed to load mcserv: %d\n", ret);
- return 1;
- }
- ret = mcInit(MC_TYPE_MC);
- if (0 > ret) {
- printf("Failed to init libmc: %d\n", ret);
- return 1;
- }
- ret = SifExecModuleBuffer(freepad_irx, size_freepad_irx, 0, nullptr, nullptr);
- if (0 > ret) {
- printf("Failed to load freepad: %d\n", ret);
- return 1;
- }
-
- padInit(0);
- padOpened[0] = (padPortOpen(0, 0, padBuf[0]) != 0);
- padOpened[1] = (padPortOpen(1, 0, padBuf[1]) != 0);
- if (!padOpened[0]) printf("Warning: failed to open pad port 0\n");
- if (!padOpened[1]) printf("Warning: failed to open pad port 1\n");
-
- // ===[ Load USB Keyboard IOP Modules ]===
- int usbdRet = SifExecModuleBuffer(usbd_irx, size_usbd_irx, 0, nullptr, nullptr);
- if (0 > usbdRet) {
- printf("Warning: failed to load usbd: %d (keyboard disabled)\n", usbdRet);
- } else {
- int kbdRet = SifExecModuleBuffer(ps2kbd_irx, size_ps2kbd_irx, 0, nullptr, nullptr);
- if (0 > kbdRet) {
- printf("Warning: failed to load ps2kbd: %d (keyboard disabled)\n", kbdRet);
- } else if (PS2KbdInit() == 0) {
- printf("Warning: PS2KbdInit failed (keyboard disabled)\n");
- } else {
- PS2KbdSetReadmode(PS2KBD_READMODE_RAW);
- PS2KbdSetBlockingMode(PS2KBD_NONBLOCKING);
- kbdAvailable = true;
- printf("USB keyboard initialized\n");
- }
- }
-
-#ifdef ENABLE_PS2_AUDIO
- // ===[ Load Audio IOP Modules ]===
- ret = SifExecModuleBuffer(freesd_irx, size_freesd_irx, 0, nullptr, nullptr);
- if (0 > ret) {
- printf("Failed to load freesd: %d\n", ret);
- }
- ret = SifExecModuleBuffer(audsrv_irx, size_audsrv_irx, 0, nullptr, nullptr);
- if (0 > ret) {
- printf("Failed to load audsrv: %d\n", ret);
- }
-#endif
-
- // Wait for pad to be ready
- drawStatusScreen(gsGlobal, gsFontM, nullptr, "Waiting for controller...", nullptr);
-
- int padState;
- do {
- padState = padGetState(0, 0);
- } while (PAD_STATE_STABLE != padState && PAD_STATE_FINDCTP1 != padState);
-
- printf("Controller initialized\n");
-
- // ===[ Loading Screen State ]===
- LoadingScreenState loadingState = {
- .gsGlobal = gsGlobal,
- .gsFontM = gsFontM,
- };
-
- // ===[ Load CONFIG.JSN ]===
- drawStatusScreen(gsGlobal, gsFontM, nullptr, "Loading CONFIG.JSN...", nullptr);
-
- char* configJsonPath = PS2Utils_createDevicePath("CONFIG.JSN");
- FILE* configFile = fopen(configJsonPath, "rb");
- JsonValue* configRoot = nullptr;
-
- if (configFile != nullptr) {
- fseek(configFile, 0, SEEK_END);
- long configSize = ftell(configFile);
- fseek(configFile, 0, SEEK_SET);
-
- char* configJsonText = safeMalloc((size_t) configSize + 1);
- size_t configBytesRead = fread(configJsonText, 1, (size_t) configSize, configFile);
- configJsonText[configBytesRead] = '\0';
- fclose(configFile);
-
- configRoot = JsonReader_parse(configJsonText);
- free(configJsonText);
- }
- free(configJsonPath);
-
- if (configRoot == nullptr) {
- drawStatusScreen(gsGlobal, gsFontM, nullptr, "CONFIG.JSN invalid or not found!", nullptr);
- while (true) {}
- }
-
- bool lazyLoadRooms = JsonReader_getBool(JsonReader_getObject(configRoot, "lazyLoadRooms"));
- StringBooleanEntry* eagerRooms = nullptr; // stb_ds string-keyed set; keys borrowed from configRoot
- JsonValue* eagerArr = JsonReader_getObject(configRoot, "eagerlyLoadedRooms");
- int n = JsonReader_arrayLength(eagerArr);
- repeat(n, i) {
- const char* name = JsonReader_getString(JsonReader_getArrayElement(eagerArr, i));
- if (name != nullptr) shput(eagerRooms, (char*) name, true);
- }
-
- // ===[ Parse data.win ]===
- drawStatusScreen(gsGlobal, gsFontM, nullptr, "Loading data.win...", nullptr);
-
- DataWin* dataWin = DataWin_parse(
- dataWinPath,
- (DataWinParserOptions) {
- .parseGen8 = true,
- .parseOptn = true,
- .parseLang = true,
- .parseExtn = false,
- .parseSond = true,
- .parseAgrp = true,
- .parseSprt = true,
- .parseBgnd = true,
- .parsePath = true,
- .parseScpt = true,
- .parseGlob = true,
- .parseShdr = true,
- .parseFont = true,
- .parseTmln = true,
- .parseObjt = true,
- .parseRoom = true,
- .parseTpag = true,
- .parseCode = true,
- .parseVari = true,
- .parseFunc = true,
- .parseStrg = true,
- .parseTxtr = false,
- .parseAudo = false,
- .skipLoadingPreciseMasksForNonPreciseSprites = true,
- .lazyLoadRooms = lazyLoadRooms,
- .eagerlyLoadedRooms = eagerRooms,
- .progressCallback = loadingScreenCallback,
- .progressCallbackUserData = &loadingState,
- }
- );
- free(dataWinPath);
- shfree(eagerRooms);
-
- bool bytecodeVersionSupported = false;
-#ifdef ENABLE_BC16
- if (dataWin->gen8.bytecodeVersion == 15 || dataWin->gen8.bytecodeVersion == 16) bytecodeVersionSupported = true;
-#endif
-#ifdef ENABLE_BC17
- if (dataWin->gen8.bytecodeVersion == 17) bytecodeVersionSupported = true;
-#endif
-
- if (!bytecodeVersionSupported) {
- char errorText[128];
- snprintf(errorText, sizeof(errorText), "Unsupported bytecode version %u!", dataWin->gen8.bytecodeVersion);
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, errorText, &loadingState);
- while (true) {}
- }
-
- {
- void* heapTop = sbrk(0);
- int32_t usedBytes = (int32_t) (uintptr_t) heapTop;
- int32_t freeBytes = MAX_MEMORY_BYTES - usedBytes;
- printf("Memory after data.win parsing: used=%d bytes (%.1f KB), total=%d bytes (%.1f KB), free=%d bytes (%.1f KB)\n", usedBytes, (double) (usedBytes / 1024.0f), MAX_MEMORY_BYTES, (double) (MAX_MEMORY_BYTES / 1024.0f), freeBytes, (double) (freeBytes / 1024.0f));
- }
-
- FileSystem* fileSystem = Ps2FileSystem_create(configRoot, dataWin->gen8.displayName);
- if (fileSystem == nullptr) {
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "CONFIG.JSN is missing the fileSystem configuration!", &loadingState);
- while (true) {}
- }
-
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Creating VM...", &loadingState);
-
- VMContext* vm = VM_create(dataWin);
-
- // ===[ Initialize Renderer ]===
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Initializing renderer...", &loadingState);
-
- Renderer* renderer = GsRenderer_create(gsGlobal);
-
- // ===[ Initialize Audio System ]===
-#ifdef ENABLE_PS2_AUDIO
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Initializing audio...", &loadingState);
- Ps2AudioSystem* ps2Audio = Ps2AudioSystem_create();
- AudioSystem* audioSystem = (AudioSystem*) ps2Audio;
-#else
- AudioSystem* audioSystem = (AudioSystem*) NoopAudioSystem_create();
-#endif
-
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Creating runner...", &loadingState);
-
- Runner* runner = Runner_create(dataWin, vm, renderer, fileSystem, audioSystem);
-
- // Parse disabledObjects from CONFIG.JSN
- JsonValue* disabledObjectsArr = JsonReader_getObject(configRoot, "disabledObjects");
- if (disabledObjectsArr != nullptr && JsonReader_isArray(disabledObjectsArr)) {
- sh_new_strdup(runner->disabledObjects);
- int disabledCount = JsonReader_arrayLength(disabledObjectsArr);
- repeat(disabledCount, i) {
- JsonValue* elem = JsonReader_getArrayElement(disabledObjectsArr, i);
- if (elem != nullptr && JsonReader_isString(elem)) {
- const char* objName = JsonReader_getString(elem);
- shput(runner->disabledObjects, objName, 1);
- printf("Disabled object: %s\n", objName);
- }
- }
- }
-
- // Parse pad mappings from CONFIG.JSN (one object per controller, both optional)
- parsePadMappings(configRoot, "controller1Mappings", &pad1Mappings, &pad1MappingCount, "controller1");
- parsePadMappings(configRoot, "controller2Mappings", &pad2Mappings, &pad2MappingCount, "controller2");
-
- {
- void* heapTop = sbrk(0);
- int32_t usedBytes = (int32_t) (uintptr_t) heapTop;
- int32_t freeBytes = MAX_MEMORY_BYTES - usedBytes;
- printf("Memory after VM and runner creation: used=%d bytes (%.1f KB), total=%d bytes (%.1f KB), free=%d bytes (%.1f KB)\n", usedBytes, (double) (usedBytes / 1024.0f), MAX_MEMORY_BYTES, (double) (MAX_MEMORY_BYTES / 1024.0f), freeBytes, (double) (freeBytes / 1024.0f));
- }
-
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Initializing first room...", &loadingState);
- Runner_initFirstRoom(runner);
-
- drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Reticulating splines...", &loadingState);
-
- // ===[ gprof Profiler Setup ]===
-#ifdef GPROF_PROFILING
- // Reset profiler to ignore any data collected during the initialization steps
- gprof_stop(nullptr, 0);
-
- // If not running from host:, load USB mass storage drivers so we can write gmon.out to mass:
- if (strcmp(deviceKey.key, "host") != 0)
- PS2Utils_loadMassStorageDrivers();
-
- gprof_start();
- fprintf(stderr, "gprof: Profiling started!\n");
-#endif
-
- Gen8* gen8 = &dataWin->gen8;
- int32_t gameW = (int32_t) gen8->defaultWindowWidth;
- int32_t gameH = (int32_t) gen8->defaultWindowHeight;
-
- // ===[ Initialize Timer ]===
- InitTimer(kBUSCLK);
- StartTimerSystemTime();
-
- // ===[ Main Loop ]===
- bool debugOverlayStartEnabled = JsonReader_getBool(JsonReader_getObject(configRoot, "debugOverlayEnabled"));
- int debugOverlayState = debugOverlayStartEnabled ? 0 : 2;
- uint16_t prevOverlayPadButtons = 0xFFFF;
- int profilerFramesInWindow = 0;
- static const int PROFILER_WINDOW_FRAMES = 60;
-#ifdef ENABLE_VM_GML_PROFILER
- char profilerOverlayText[4096];
-#endif
- while (!runner->shouldExit) {
- u64 frameStartTime = GetTimerSystemTime();
- // ===[ Poll Controller (always poll every vsync) ]===
- // NOTE: We do NOT call RunnerKeyboard_beginFrame here! Pressed/released edges accumulate across vsyncs so that quick taps on non-game-frame
- // vsyncs are not lost
- //
- // beginFrame is called after the game consumes input.
-
- if (padOpened[0]) pollPad(runner, 0, pad1Mappings, pad1MappingCount, &prevButtons[0], &padWasStable[0]);
- if (padOpened[1]) pollPad(runner, 1, pad2Mappings, pad2MappingCount, &prevButtons[1], &padWasStable[1]);
-
- // ===[ Poll USB Keyboard ]===
- // Drain all pending RAW events this vsync so press/release edges aren't dropped.
- if (kbdAvailable) {
- PS2KbdRawKey rawKey;
- while (PS2KbdReadRaw(&rawKey) > 0) {
- int32_t gmlKey = hidUsageToGmlKey(rawKey.key);
-
- // Track shift modifier locally so we can pick the correct glyph for onCharacter.
- if (rawKey.key == 0xE1 || rawKey.key == 0xE5) {
- kbdShiftHeld = (rawKey.state == PS2KBD_RAWKEY_DOWN);
- }
-
- if (rawKey.state == PS2KBD_RAWKEY_DOWN) {
- if (gmlKey >= 0) RunnerKeyboard_onKeyDown(runner->keyboard, gmlKey);
- unsigned int ch = hidUsageToAsciiChar(rawKey.key, kbdShiftHeld);
- if (ch != 0) RunnerKeyboard_onCharacter(runner->keyboard, ch);
- } else if (rawKey.state == PS2KBD_RAWKEY_UP) {
- if (gmlKey >= 0) RunnerKeyboard_onKeyUp(runner->keyboard, gmlKey);
- }
- }
- }
-
- // R2 on pad1 removes speed cap (ignore waiting for vsync)
- bool speedCapRemoved = padWasStable[0] && ((prevButtons[0] & PAD_R2) == 0);
-
- // Go to next room
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEUP)) {
- DataWin* dw = runner->dataWin;
- if ((int32_t) dw->gen8.roomOrderCount > runner->currentRoomOrderPosition + 1) {
- int32_t nextIdx = dw->gen8.roomOrder[runner->currentRoomOrderPosition + 1];
- runner->pendingRoom = nextIdx;
- runner->audioSystem->vtable->stopAll(runner->audioSystem);
- fprintf(stderr, "Debug: Going to next room -> %s\n", dw->room.rooms[nextIdx].name);
- }
- }
-
- // Go to previous room
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEDOWN)) {
- DataWin* dw = runner->dataWin;
- forEachIndexed(Room, room, i, dw->room.rooms, dw->room.count) {
- if (strcmp(room->name, "room_asrielappears") == 0) {
- runner->pendingRoom = i;
- runner->audioSystem->vtable->stopAll(runner->audioSystem);
- break;
- }
- }
- }
-
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F12)) {
- debugOverlayState = (debugOverlayState + 1) % 3;
-#ifdef ENABLE_VM_GML_PROFILER
- Profiler_setEnabled(&vm->profiler, debugOverlayState == 1);
- profilerFramesInWindow = 0;
- profilerOverlayText[0] = '\0';
-#endif
- }
-
- // Reset global interact state because I HATE when I get stuck while moving through rooms
- if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F10)) {
- int32_t interactVarId = shget(runner->vmContext->globalVarNameMap, "interact");
-
- runner->vmContext->globalVars[interactVarId] = RValue_makeInt32(0);
- printf("Changed global.interact [%d] value!\n", interactVarId);
- }
-
- // ===[ Game Logic ]===
- uint32_t roomSpeed = runner->currentRoom->speed;
-
- u64 stepStartTime = GetTimerSystemTime();
- Runner_step(runner);
- u64 stepEndTime = GetTimerSystemTime();
-
- gsKit_clear(gsGlobal, GS_SETREG_RGBAQ(0x00, 0x00, 0x00, 0x80, 0x00));
-
- renderer->vtable->beginFrame(renderer, gameW, gameH, 640, 448);
-
- // Clear with room background color
- if (runner->drawBackgroundColor) {
- uint8_t bgR = BGR_R(runner->backgroundColor);
- uint8_t bgG = BGR_G(runner->backgroundColor);
- uint8_t bgB = BGR_B(runner->backgroundColor);
- u64 bgColor = GS_SETREG_RGBAQ(bgR, bgG, bgB, 0x80, 0x00);
- gsKit_prim_sprite(gsGlobal, 0, 0, 640, 448, 0, bgColor);
- }
-
- // Render views
- u64 drawStartTime = GetTimerSystemTime();
- Runner_drawViews(runner, gameW, gameH, 1.0f, 1.0f, false);
- u64 drawEndTime = GetTimerSystemTime();
-
- runner->viewCurrent = 0;
-
- renderer->vtable->endFrame(renderer);
-
- // Clear pressed/released edges after both Step and Draw have consumed input
- // This MUST be after Runner_draw because games CAN handle input in Draw events (e.g. Undertale's naming screen)
- RunnerKeyboard_beginFrame(runner->keyboard);
-
- u64 audioStartTime = GetTimerSystemTime();
- // Update audio system (gain fading, stream to audsrv)
- float dt = 1.0f / (float) roomSpeed;
- if (0.0f > dt) dt = 0.0f;
- if (dt > 0.1f) dt = 0.1f;
- runner->audioSystem->vtable->update(runner->audioSystem, dt);
- u64 audioEndTime = GetTimerSystemTime();
-
- u64 runnerEndTime = GetTimerSystemTime();
- u64 duration = runnerEndTime - frameStartTime;
- u64 stepDuration = stepEndTime - stepStartTime;
- u64 drawDuration = drawEndTime - drawStartTime;
- u64 audioDuration = audioEndTime - audioStartTime;
- float tickTime = (float) duration / (float) (kBUSCLK / 1000);
- float stepTime = (float) stepDuration / (float) (kBUSCLK / 1000);
- float drawTime = (float) drawDuration / (float) (kBUSCLK / 1000);
- float audioTime = (float) audioDuration / (float) (kBUSCLK / 1000);
-
- // ===[ Debug Overlay ]===
- if (debugOverlayState == 0 || debugOverlayState == 1) {
- u64 debugColor = GS_SETREG_RGBAQ(0xFF, 0xFF, 0xFF, 0x80, 0x00);
- char debugText[512];
- uint32_t vramFreeBytes = GS_VRAM_SIZE - gsGlobal->CurrentPointer;
-
- // Count atlases loaded in VRAM and EE RAM cache
- GsRenderer* gsRenderer = (GsRenderer*) renderer;
- uint32_t vramAtlasCount = 0;
- uint32_t eeramAtlasCount = 0;
- repeat(gsRenderer->atlasCount, ai) {
- if (gsRenderer->atlasToChunk[ai] >= 0) vramAtlasCount++;
- if (gsRenderer->eeCacheEntries[ai].atlasId >= 0) eeramAtlasCount++;
- }
-
- int freeBytes = heapCeilingBytes - mallinfo().uordblks;
-
- const char* roomName = runner->currentRoom != nullptr && runner->currentRoom->name != nullptr ? runner->currentRoom->name : "?";
-
- const char* thrashIndicator = "";
- if (gsRenderer->chunksNeededThisFrame > gsRenderer->chunkCount) {
- thrashIndicator = gsRenderer->diskLoadsThisFrame > 0 ? " [RAM+DISK THRASHING]" : " [RAM THRASHING]";
- } else if (gsRenderer->diskLoadsThisFrame > 0) {
- thrashIndicator = " [DISK LOAD]";
- }
-
- snprintf(debugText, sizeof(debugText), "Room: %s\nTick: %.2fms\nStep: %.2fms\nDraw: %.2fms\nAudio: %.2fms\nFree: %d bytes\nVRAM Free: %lu bytes\nRoom Speed: %u%s\nAtlas: (%u, %u, %u) [%u/%u]%s\nInstances: %d\nStructs: %d", roomName, (double) tickTime, (double) stepTime, (double) drawTime, (double) audioTime, freeBytes, (unsigned long) vramFreeBytes, roomSpeed, speedCapRemoved ? " [UNCAPPED]" : "", vramAtlasCount, eeramAtlasCount, gsRenderer->atlasCount, gsRenderer->chunksNeededThisFrame, gsRenderer->chunkCount, thrashIndicator, (int) arrlen(runner->instances), (int) arrlen(runner->structInstances));
- gsKit_fontm_print_scaled(gsGlobal, gsFontM, 10.0f, 10.0f, 10, 0.6f, debugColor, debugText);
-
- if (debugOverlayState == 1) {
- float profilerY = 10.0f + (15.6f * 10.0f) + 6.0f;
-
-#ifdef ENABLE_VM_GML_PROFILER
- profilerFramesInWindow++;
- if (profilerFramesInWindow >= PROFILER_WINDOW_FRAMES) {
- char* profilerReport = Profiler_createReport(vm->profiler, 25, profilerFramesInWindow);
- if (profilerReport != nullptr) {
- snprintf(profilerOverlayText, sizeof(profilerOverlayText), "%s", profilerReport);
- free(profilerReport);
- }
- Profiler_reset(vm->profiler);
- profilerFramesInWindow = 0;
- }
- const char* profilerDisplay = profilerOverlayText[0] != '\0' ? profilerOverlayText : "GML Profiler (collecting...)";
- gsKit_fontm_print_scaled(gsGlobal, gsFontM, 10.0f, profilerY, 10, 0.35f, debugColor, profilerDisplay);
-#else
- gsKit_fontm_print_scaled(gsGlobal, gsFontM, 10.0f, profilerY, 10, 0.35f, debugColor, "Butterscotch GML Profiler is disabled on this build :(");
-#endif
- }
- }
-
- // Execute draw queue and flip buffers
- gsKit_queue_exec(gsGlobal);
- gsKit_sync_flip(gsGlobal);
-
- // Busy-wait until enough time has elapsed for this frame if needed
- if (!speedCapRemoved && roomSpeed > 0) {
- u64 targetTicks = kBUSCLK / roomSpeed;
- while (targetTicks > GetTimerSystemTime() - frameStartTime) {
- // spin spin spin!
- }
- }
- }
-
- // ===[ gprof Profiler Output ]===
-#ifdef GPROF_PROFILING
- {
- const char* gprofPath;
- if (strcmp(deviceKey.key, "host") == 0) {
- gprofPath = "host:gmon.out";
- } else {
- gprofPath = "mass:gmon.out";
- }
- fprintf(stderr, "gprof: Writing profiling data to %s\n", gprofPath);
- gprof_stop(gprofPath, 1);
- fprintf(stderr, "gprof: Done\n");
- }
-#endif
-
- // For some god awful reason PCSX2 crashes when destroying the audio system (???)
- // runner->audioSystem->vtable->destroy(runner->audioSystem);
- runner->audioSystem = nullptr;
- renderer->vtable->destroy(renderer);
- DataWin_free(dataWin);
-
- return 0;
-}
diff --git a/src/ps2/ps2_audio_system.c b/src/ps2/ps2_audio_system.c
deleted file mode 100644
index 848b1353..00000000
--- a/src/ps2/ps2_audio_system.c
+++ /dev/null
@@ -1,1350 +0,0 @@
-#include "ps2_audio_system.h"
-#include "ps2_utils.h"
-#include "utils.h"
-
-#include
-#include
-#include
-#include
-#include
-
-// ===[ IMA ADPCM Tables ]===
-
-static const int16_t IMA_STEP_TABLE[89] = {
- 7, 8, 9, 10, 11, 12, 13, 14,
- 16, 17, 19, 21, 23, 25, 28, 31,
- 34, 37, 41, 45, 50, 55, 60, 66,
- 73, 80, 88, 97, 107, 118, 130, 143,
- 157, 173, 190, 209, 230, 253, 279, 307,
- 337, 371, 408, 449, 494, 544, 598, 658,
- 724, 796, 876, 963, 1060, 1166, 1282, 1411,
- 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024,
- 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484,
- 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
- 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
- 32767
-};
-
-static const int8_t IMA_INDEX_TABLE[16] = {
- -1, -1, -1, -1, 2, 4, 6, 8,
- -1, -1, -1, -1, 2, 4, 6, 8
-};
-
-// ===[ IMA ADPCM Decoder ]===
-
-// Decode a block of IMA ADPCM data, updating predictor/stepIndex state in place
-// Returns the number of samples written to outPcm (2 per input byte)
-static uint32_t imaAdpcmDecodeBlock(const uint8_t* adpcmData, uint32_t adpcmSize, int16_t* outPcm, int32_t* predictor, int32_t* stepIndex) {
- uint32_t samplesWritten = 0;
-
- repeat(adpcmSize, i) {
- uint8_t byte = adpcmData[i];
-
- // Process two nibbles per byte (low nibble first, then high)
- for (int nibbleIdx = 0; 2 > nibbleIdx; nibbleIdx++) {
- uint8_t nibble = (nibbleIdx == 0) ? (byte & 0x0F) : ((byte >> 4) & 0x0F);
-
- int32_t step = IMA_STEP_TABLE[*stepIndex];
- int32_t delta = step >> 3;
- if (nibble & 1) delta += step >> 2;
- if (nibble & 2) delta += step >> 1;
- if (nibble & 4) delta += step;
- if (nibble & 8) delta = -delta;
-
- *predictor += delta;
- if (*predictor > 32767) *predictor = 32767;
- if (-32768 > *predictor) *predictor = -32768;
-
- *outPcm++ = (int16_t) *predictor;
- samplesWritten++;
-
- *stepIndex += IMA_INDEX_TABLE[nibble];
- if (0 > *stepIndex) *stepIndex = 0;
- if (*stepIndex > 88) *stepIndex = 88;
- }
- }
-
- return samplesWritten;
-}
-
-// Convenience wrapper for full-buffer decode (SFX path)
-static void imaAdpcmDecode(const uint8_t* adpcmData, uint32_t adpcmSize, int16_t* outPcm) {
- int32_t predictor = 0;
- int32_t stepIndex = 0;
- imaAdpcmDecodeBlock(adpcmData, adpcmSize, outPcm, &predictor, &stepIndex);
-}
-
-// ===[ SOUNDBNK.BIN Parser ]===
-
-static void parseSoundBank(Ps2AudioSystem* ps2) {
- char* path = PS2Utils_createDevicePath("SOUNDBNK.BIN");
-
- FILE* f = fopen(path, "rb");
- free(path);
- if (f == nullptr) {
- fprintf(stderr, "PS2AudioSystem: Could not open SOUNDBNK.BIN\n");
- return;
- }
-
- // Header: version(u8) + sondEntryCount(u16) + audoEntryCount(u16) + musEntryCount(u16) = 7 bytes
- uint8_t version;
- fread(&version, 1, 1, f);
- fread(&ps2->sondEntryCount, 2, 1, f);
- fread(&ps2->audoEntryCount, 2, 1, f);
- fread(&ps2->musEntryCount, 2, 1, f);
-
- fprintf(stderr, "PS2AudioSystem: SOUNDBNK v%d, %d SOND entries, %d AUDO entries, %d MUS entries\n", version, ps2->sondEntryCount, ps2->audoEntryCount, ps2->musEntryCount);
-
- // Parse SOND entries (12 bytes each)
- ps2->sondEntries = safeMalloc(ps2->sondEntryCount * sizeof(Ps2SondEntry));
- for (int i = 0; ps2->sondEntryCount > i; i++) {
- uint8_t buf[12];
- fread(buf, 12, 1, f);
- ps2->sondEntries[i].audoIndex = *(uint16_t*) &buf[0];
- ps2->sondEntries[i].flags = *(uint32_t*) &buf[2];
- ps2->sondEntries[i].volume = *(int16_t*) &buf[6];
- ps2->sondEntries[i].pitch = *(int16_t*) &buf[8];
- // buf[10..11] reserved
- }
-
- // Parse AUDO entries (20 bytes each)
- ps2->audoEntries = safeMalloc(ps2->audoEntryCount * sizeof(Ps2AudoEntry));
- for (int i = 0; ps2->audoEntryCount > i; i++) {
- uint8_t buf[20];
- fread(buf, 20, 1, f);
- ps2->audoEntries[i].dataOffset = *(uint32_t*) &buf[0];
- ps2->audoEntries[i].dataSize = *(uint32_t*) &buf[4];
- ps2->audoEntries[i].sampleRate = *(uint16_t*) &buf[8];
- ps2->audoEntries[i].channels = buf[10];
- ps2->audoEntries[i].bitsPerSample = buf[11];
- ps2->audoEntries[i].format = buf[12];
- // buf[13..15] reserved
- ps2->audoEntries[i].sampleCount = *(uint32_t*) &buf[16];
- }
-
- // Parse MUS string table + MUS entries
- ps2->musEntries = safeMalloc(ps2->musEntryCount * sizeof(Ps2MusEntry));
-
- // First pass: read the string table (u8 nameLength + name bytes per entry)
- repeat(ps2->musEntryCount, i) {
- uint8_t nameLen;
- fread(&nameLen, 1, 1, f);
- char* name = safeMalloc(nameLen + 1);
- fread(name, 1, nameLen, f);
- name[nameLen] = '\0';
- ps2->musEntries[i].name = name;
- }
-
- // Second pass: read the MUS entries (16 bytes each)
- repeat(ps2->musEntryCount, i) {
- uint8_t buf[16];
- fread(buf, 16, 1, f);
- ps2->musEntries[i].dataOffset = *(uint32_t*) &buf[0];
- ps2->musEntries[i].dataSize = *(uint32_t*) &buf[4];
- ps2->musEntries[i].sampleRate = *(uint16_t*) &buf[8];
- ps2->musEntries[i].channels = buf[10];
- ps2->musEntries[i].format = buf[11];
- ps2->musEntries[i].sampleCount = *(uint32_t*) &buf[12];
- }
-
- if (ps2->musEntryCount > 0) {
- fprintf(stderr, "PS2AudioSystem: Loaded %d MUS entries\n", ps2->musEntryCount);
- }
-
- fclose(f);
-}
-
-// ===[ SOUNDS.BIN File Handle ]===
-
-static void openSoundsBin(Ps2AudioSystem* ps2) {
- // SOUNDS.BIN is always alongside the ELF on the boot device, not in the FileSystem mappings
- // We keep the file handle open and read on demand to avoid loading everything into EE RAM
- char* path = PS2Utils_createDevicePath("SOUNDS.BIN");
-
- ps2->soundsFile = fopen(path, "rb");
- if (ps2->soundsFile == nullptr) {
- fprintf(stderr, "PS2AudioSystem: Could not open SOUNDS.BIN at %s\n", path);
- free(path);
- return;
- }
-
- fprintf(stderr, "PS2AudioSystem: Opened SOUNDS.BIN for streaming (%s)\n", path);
- free(path);
-}
-
-// ===[ LRU Decoded PCM Cache (SFX) ]===
-
-static DecodedPcmEntry* cacheGet(Ps2AudioSystem* ps2, int32_t audoIndex) {
- for (int i = 0; LRU_CACHE_SIZE > i; i++) {
- if (ps2->cacheEntries[i].audoIndex == audoIndex) {
- ps2->cacheEntries[i].lastAccessCounter = ++ps2->cacheAccessCounter;
- return &ps2->cacheEntries[i];
- }
- }
- return nullptr;
-}
-
-static bool cacheIsInUse(Ps2AudioSystem* ps2, int32_t audoIndex) {
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- if (ps2->instances[i].active && ps2->instances[i].audoIndex == audoIndex) {
- return true;
- }
- }
- return false;
-}
-
-static DecodedPcmEntry* cacheInsert(Ps2AudioSystem* ps2, int32_t audoIndex) {
- Ps2AudoEntry* audo = &ps2->audoEntries[audoIndex];
-
- // Compute decoded sample count: IMA ADPCM = 2 samples per byte
- uint32_t sampleCount = audo->dataSize * 2;
- uint32_t pcmBytes = sampleCount * sizeof(int16_t);
-
- // Find a free slot
- DecodedPcmEntry* slot = nullptr;
- for (int i = 0; LRU_CACHE_SIZE > i; i++) {
- if (0 > ps2->cacheEntries[i].audoIndex) {
- slot = &ps2->cacheEntries[i];
- break;
- }
- }
-
- // No free slot: evict LRU entry not in active use
- if (slot == nullptr) {
- uint32_t oldestAccess = UINT32_MAX;
- for (int i = 0; LRU_CACHE_SIZE > i; i++) {
- DecodedPcmEntry* entry = &ps2->cacheEntries[i];
- if (!cacheIsInUse(ps2, entry->audoIndex) && oldestAccess > entry->lastAccessCounter) {
- oldestAccess = entry->lastAccessCounter;
- slot = entry;
- }
- }
-
- if (slot == nullptr) {
- // fprintf(stderr, "PS2AudioSystem: Cache full, all entries in use! Cannot decode audoIndex %" PRId32 "\n", audoIndex);
- return nullptr;
- }
-
- // Free the evicted entry's PCM data
- free(slot->pcmData);
- }
-
- // Read compressed ADPCM data from SOUNDS.BIN on demand
- uint8_t* adpcmBuf = safeMalloc(audo->dataSize);
- fseek(ps2->soundsFile, (long) audo->dataOffset, SEEK_SET);
- fread(adpcmBuf, 1, audo->dataSize, ps2->soundsFile);
-
- // Decode IMA ADPCM into new PCM buffer
- slot->pcmData = safeMalloc(pcmBytes);
- imaAdpcmDecode(adpcmBuf, audo->dataSize, slot->pcmData);
- free(adpcmBuf);
-
- slot->audoIndex = audoIndex;
- slot->pcmSampleCount = sampleCount;
- slot->pcmDataBytes = pcmBytes;
- slot->lastAccessCounter = ++ps2->cacheAccessCounter;
-
- return slot;
-}
-
-// ===[ Streaming Music ]===
-
-// Fill one buffer of a music stream by reading ADPCM from SOUNDS.BIN and decoding
-static void streamFillBuffer(Ps2AudioSystem* ps2, Ps2MusicStream* stream, int bufferIndex) {
- if (stream->fileOffset >= stream->fileEndOffset) {
- // No more data to read
- stream->bufferSampleCount[bufferIndex] = 0;
- return;
- }
-
- // How many ADPCM bytes remain for this track?
- uint32_t remaining = stream->fileEndOffset - stream->fileOffset;
- uint32_t toRead = STREAM_ADPCM_CHUNK_BYTES;
- if (toRead > remaining) toRead = remaining;
-
- // Read ADPCM chunk from disc
- uint8_t adpcmBuf[STREAM_ADPCM_CHUNK_BYTES];
- fseek(ps2->soundsFile, (long) stream->fileOffset, SEEK_SET);
- size_t bytesRead = fread(adpcmBuf, 1, toRead, ps2->soundsFile);
- stream->fileOffset += (uint32_t) bytesRead;
-
- // Decode into the target buffer, continuing decoder state
- uint32_t samples = imaAdpcmDecodeBlock(adpcmBuf, (uint32_t) bytesRead, stream->buffers[bufferIndex], &stream->decoderPredictor, &stream->decoderStepIndex);
- stream->bufferSampleCount[bufferIndex] = samples;
-
- if (stream->fileOffset >= stream->fileEndOffset) {
- stream->endOfTrack = true;
- }
-}
-
-// Reset a stream to the beginning of its track (for looping)
-static void streamResetToStart(Ps2AudioSystem* ps2, Ps2MusicStream* stream) {
- stream->fileOffset = stream->fileStartOffset;
- stream->decoderPredictor = 0;
- stream->decoderStepIndex = 0;
- stream->endOfTrack = false;
- stream->readPosition = 0;
- stream->readPositionFrac = 0;
-
- // Fill both buffers from the start
- streamFillBuffer(ps2, stream, 0);
- streamFillBuffer(ps2, stream, 1);
- stream->activeBuffer = 0;
- stream->needsRefill = false;
-}
-
-// ===[ SFX Instance Helpers ]===
-
-static Ps2SoundInstance* findFreeSlot(Ps2AudioSystem* ps2) {
- // First pass: find an inactive slot
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- if (!ps2->instances[i].active) {
- return &ps2->instances[i];
- }
- }
-
- // Second pass: evict the lowest-priority ended sound
- Ps2SoundInstance* best = nullptr;
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- Ps2SoundInstance* inst = &ps2->instances[i];
- if (inst->positionInt >= inst->totalSamples && !inst->loop) {
- if (best == nullptr || best->priority > inst->priority) {
- best = inst;
- }
- }
- }
-
- if (best != nullptr) {
- best->active = false;
- }
-
- return best;
-}
-
-static Ps2SoundInstance* findSfxInstanceById(Ps2AudioSystem* ps2, int32_t instanceId) {
- int32_t slotIndex = instanceId - PS2_SOUND_INSTANCE_ID_BASE;
- if (0 > slotIndex || slotIndex >= MAX_PS2_SOUND_INSTANCES) return nullptr;
- Ps2SoundInstance* inst = &ps2->instances[slotIndex];
- if (!inst->active || inst->instanceId != instanceId) return nullptr;
- return inst;
-}
-
-// Find a music stream by instance ID
-static Ps2MusicStream* findMusicStreamById(Ps2AudioSystem* ps2, int32_t instanceId) {
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].instanceId == instanceId) {
- return &ps2->musicStreams[i];
- }
- }
- return nullptr;
-}
-
-// Get the sample rate for a music stream (from AUDO or MUS entry)
-static uint16_t getMusicStreamSampleRate(Ps2AudioSystem* ps2, Ps2MusicStream* stream) {
- if (stream->soundIndex >= PS2_AUDIO_STREAM_INDEX_BASE) {
- int32_t musIndex = stream->soundIndex - PS2_AUDIO_STREAM_INDEX_BASE;
- if (ps2->musEntryCount > musIndex) return ps2->musEntries[musIndex].sampleRate;
- return AUDSRV_OUTPUT_FREQ;
- }
- if ((uint16_t) stream->audoIndex < ps2->audoEntryCount) return ps2->audoEntries[stream->audoIndex].sampleRate;
- return AUDSRV_OUTPUT_FREQ;
-}
-
-// ===[ Software Mixer ]===
-
-static void mixAudio(Ps2AudioSystem* ps2, int16_t* outBuf, int32_t samplePairs) {
- int32_t* accum = ps2->mixAccum;
- memset(accum, 0, samplePairs * sizeof(int32_t));
-
- // ===[ Mix SFX instances (from LRU cache) ]===
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- Ps2SoundInstance* inst = &ps2->instances[i];
- if (!inst->active || inst->paused) continue;
-
- DecodedPcmEntry* cache = cacheGet(ps2, inst->audoIndex);
- if (cache == nullptr) continue;
-
- if (inst->positionInt >= inst->totalSamples) {
- if (inst->loop) {
- inst->positionInt = 0;
- inst->positionFrac = 0;
- } else {
- inst->active = false;
- continue;
- }
- }
-
- // Hoist per-instance constants out of the per-sample loop
- const int16_t* pcm = cache->pcmData;
- uint32_t totalSamples = inst->totalSamples;
- bool loop = inst->loop;
- float gain = inst->currentGain * inst->sondVolume * ps2->masterGain;
- int32_t gainQ15 = (int32_t) (gain * 32768.0f);
- Ps2AudoEntry* audo = &ps2->audoEntries[inst->audoIndex];
- float stepRate = inst->pitch * inst->sondPitch * ((float) audo->sampleRate / (float) AUDSRV_OUTPUT_FREQ);
- uint32_t stepInt = (uint32_t) stepRate;
- uint32_t stepFrac = (uint32_t) ((stepRate - (float) stepInt) * 4294967296.0f);
- bool nativeRate = (stepInt == 1 && stepFrac == 0);
-
- uint32_t posInt = inst->positionInt;
- uint32_t posFrac = inst->positionFrac;
- bool ended = false;
-
- if (nativeRate) {
- // Fast path: no resampling, no fractional position (most SFX at 22050 Hz)
- for (int32_t s = 0; samplePairs > s; s++) {
- int32_t sample = pcm[posInt];
- accum[s] += (sample * gainQ15) >> 15;
- posInt++;
-
- if (posInt >= totalSamples) {
- if (loop) {
- posInt = 0;
- } else {
- ended = true;
- break;
- }
- }
- }
- } else {
- // Resampling path: linear interpolation with 32.32 fixed-point position
- for (int32_t s = 0; samplePairs > s; s++) {
- int32_t idx0 = (int32_t) posInt;
- int32_t idx1 = idx0 + 1;
- if ((uint32_t) idx1 >= totalSamples) idx1 = idx0;
-
- int32_t s0 = pcm[idx0];
- int32_t s1 = pcm[idx1];
- int32_t frac = (int32_t) (posFrac >> 16);
- int32_t sample = s0 + ((s1 - s0) * frac >> 16);
-
- accum[s] += (sample * gainQ15) >> 15;
-
- uint32_t oldFrac = posFrac;
- posFrac += stepFrac;
- if (oldFrac > posFrac) posInt++;
- posInt += stepInt;
-
- if (posInt >= totalSamples) {
- if (loop) {
- posInt = posInt % totalSamples;
- posFrac = 0;
- } else {
- ended = true;
- break;
- }
- }
- }
- }
-
- inst->positionInt = posInt;
- inst->positionFrac = posFrac;
- if (ended) inst->active = false;
- }
-
- // ===[ Mix streaming music instances ]===
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (!stream->active || stream->paused) continue;
-
- // Hoist per-stream constants (pitch/sampleRate don't change mid-mix)
- float gain = stream->currentGain * stream->sondVolume * ps2->masterGain;
- int32_t gainQ15 = (int32_t) (gain * 32768.0f);
- uint16_t streamSampleRate = getMusicStreamSampleRate(ps2, stream);
- float stepRate = stream->pitch * stream->sondPitch * ((float) streamSampleRate / (float) AUDSRV_OUTPUT_FREQ);
- uint32_t stepInt = (uint32_t) stepRate;
- uint32_t stepFrac = (uint32_t) ((stepRate - (float) stepInt) * 4294967296.0f);
- bool nativeRate = (stepInt == 1 && stepFrac == 0);
-
- for (int32_t s = 0; samplePairs > s; s++) {
- uint32_t bufSamples = stream->bufferSampleCount[stream->activeBuffer];
-
- // Check if we've exhausted the active buffer
- if (stream->readPosition >= bufSamples) {
- if (stream->needsRefill && stream->endOfTrack) {
- if (stream->loop) {
- streamResetToStart(ps2, stream);
- bufSamples = stream->bufferSampleCount[stream->activeBuffer];
- } else {
- stream->active = false;
- break;
- }
- } else {
- // Swap to the back buffer (which should have been refilled)
- stream->activeBuffer ^= 1;
- stream->readPosition = 0;
- stream->readPositionFrac = 0;
- stream->needsRefill = true;
- bufSamples = stream->bufferSampleCount[stream->activeBuffer];
-
- if (bufSamples == 0) {
- if (stream->loop) {
- streamResetToStart(ps2, stream);
- bufSamples = stream->bufferSampleCount[stream->activeBuffer];
- } else {
- stream->active = false;
- break;
- }
- }
- }
- }
-
- int16_t* buf = stream->buffers[stream->activeBuffer];
- int32_t sample;
-
- if (nativeRate) {
- sample = buf[stream->readPosition];
- } else {
- int32_t idx0 = (int32_t) stream->readPosition;
- int32_t idx1 = idx0 + 1;
- if ((uint32_t) idx1 >= bufSamples) idx1 = idx0;
-
- int32_t s0 = buf[idx0];
- int32_t s1 = buf[idx1];
- int32_t frac = (int32_t) (stream->readPositionFrac >> 16);
- sample = s0 + ((s1 - s0) * frac >> 16);
- }
-
- accum[s] += (sample * gainQ15) >> 15;
-
- uint32_t oldFrac = stream->readPositionFrac;
- stream->readPositionFrac += stepFrac;
- if (oldFrac > stream->readPositionFrac) stream->readPosition++;
- stream->readPosition += stepInt;
- }
- }
-
- // ===[ Clamp mono accumulator and duplicate into interleaved stereo ]===
- for (int32_t s = 0; samplePairs > s; s++) {
- int32_t v = accum[s];
- if (v > 32767) v = 32767;
- if (-32768 > v) v = -32768;
- int16_t v16 = (int16_t) v;
- outBuf[s * 2] = v16;
- outBuf[s * 2 + 1] = v16;
- }
-}
-
-// ===[ Vtable Implementations ]===
-
-static void ps2Init(AudioSystem* audio, MAYBE_UNUSED DataWin* dataWin, MAYBE_UNUSED FileSystem* fileSystem) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
-
- // Parse sound bank index
- parseSoundBank(ps2);
- if (ps2->sondEntries == nullptr || ps2->audoEntries == nullptr) {
- fprintf(stderr, "PS2AudioSystem: Failed to parse SOUNDBNK.BIN, audio disabled\n");
- return;
- }
-
- // Open SOUNDS.BIN for streaming (kept open for on-demand reads)
- openSoundsBin(ps2);
- if (ps2->soundsFile == nullptr) {
- fprintf(stderr, "PS2AudioSystem: Failed to open SOUNDS.BIN, audio disabled\n");
- return;
- }
-
- // Initialize LRU cache (all slots empty)
- for (int i = 0; LRU_CACHE_SIZE > i; i++) {
- ps2->cacheEntries[i].audoIndex = -1;
- ps2->cacheEntries[i].pcmData = nullptr;
- }
-
- // Initialize sound instances
- memset(ps2->instances, 0, sizeof(ps2->instances));
- memset(ps2->musicStreams, 0, sizeof(ps2->musicStreams));
- ps2->nextInstanceCounter = 0;
- ps2->masterGain = 1.0f;
-
- // Initialize audsrv
- int ret = audsrv_init();
- if (ret != 0) {
- fprintf(stderr, "PS2AudioSystem: audsrv_init failed (%d)\n", ret);
- return;
- }
-
- struct audsrv_fmt_t format;
- format.freq = AUDSRV_OUTPUT_FREQ;
- format.bits = 16;
- format.channels = 2;
-
- ret = audsrv_set_format(&format);
- if (ret != 0) {
- fprintf(stderr, "PS2AudioSystem: audsrv_set_format failed (%d)\n", ret);
- audsrv_quit();
- return;
- }
-
- audsrv_set_volume(MAX_VOLUME);
-
- ps2->initialized = true;
- fprintf(stderr, "PS2AudioSystem: Initialized (output: %d Hz, 16-bit, stereo)\n", AUDSRV_OUTPUT_FREQ);
-}
-
-static void ps2Destroy(AudioSystem* audio) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
-
- if (ps2->initialized) {
- audsrv_stop_audio();
- audsrv_quit();
- }
-
- // Close SOUNDS.BIN file handle
- if (ps2->soundsFile != nullptr) {
- fclose(ps2->soundsFile);
- }
-
- // Free all cached PCM data
- for (int i = 0; LRU_CACHE_SIZE > i; i++) {
- free(ps2->cacheEntries[i].pcmData);
- }
-
- // Free sound bank entries
- free(ps2->sondEntries);
- free(ps2->audoEntries);
-
- // Free MUS entry names
- repeat(ps2->musEntryCount, i) {
- free(ps2->musEntries[i].name);
- }
- free(ps2->musEntries);
-
- free(ps2);
-}
-
-static void ps2Update(AudioSystem* audio, float deltaTime) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- if (!ps2->initialized) return;
-
- // Cap deltaTime to prevent large fades on lag spikes
- if (deltaTime > 0.1f) deltaTime = 0.1f;
-
- // Update gain fading on SFX instances
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- Ps2SoundInstance* inst = &ps2->instances[i];
- if (!inst->active) continue;
-
- if (inst->fadeTimeRemaining > 0.0f) {
- inst->fadeTimeRemaining -= deltaTime;
- if (0.0f >= inst->fadeTimeRemaining) {
- inst->fadeTimeRemaining = 0.0f;
- inst->currentGain = inst->targetGain;
- } else {
- float t = 1.0f - (inst->fadeTimeRemaining / inst->fadeTotalTime);
- inst->currentGain = inst->startGain + (inst->targetGain - inst->startGain) * t;
- }
- }
- }
-
- // Update gain fading on music streams
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (!stream->active) continue;
-
- if (stream->fadeTimeRemaining > 0.0f) {
- stream->fadeTimeRemaining -= deltaTime;
- if (0.0f >= stream->fadeTimeRemaining) {
- stream->fadeTimeRemaining = 0.0f;
- stream->currentGain = stream->targetGain;
- } else {
- float t = 1.0f - (stream->fadeTimeRemaining / stream->fadeTotalTime);
- stream->currentGain = stream->startGain + (stream->targetGain - stream->startGain) * t;
- }
- }
- }
-
- // Refill music stream back buffers (do disc I/O here, outside the mixer loop)
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (!stream->active || !stream->needsRefill) continue;
- // fprintf(stderr, "PS2AudioSystem: Filling music stream %d back buffers...\n", stream->soundIndex);
-
- int backBuffer = stream->activeBuffer ^ 1;
- streamFillBuffer(ps2, stream, backBuffer);
- stream->needsRefill = false;
- }
-
- // Fill audsrv ring buffer
- int32_t chunkBytes = MIX_BUFFER_SAMPLES * 2 * (int32_t) sizeof(int16_t);
- while (audsrv_available() >= chunkBytes) {
- // fprintf(stderr, "PS2AudioSystem: Filling audsrv ring buffer... audsrv_available: %d, chunkBytes: %d\n", audsrv_available(), chunkBytes);
- mixAudio(ps2, ps2->mixBuffer, MIX_BUFFER_SAMPLES);
- audsrv_play_audio((char*) ps2->mixBuffer, chunkBytes);
- }
-
- // fprintf(stderr, "PS2AudioSystem: Finished ticking the audio system\n");
-}
-
-static int32_t ps2PlaySound(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop) {
- // fprintf(stderr, "PS2AudioSystem: Attempting to play sound index %d with priority %d, should loop? %d\n", soundIndex, priority, loop);
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- if (!ps2->initialized) return -1;
-
- // Check if this is a MUS stream index (created by audio_create_stream)
- if (soundIndex >= PS2_AUDIO_STREAM_INDEX_BASE) {
- int32_t musIndex = soundIndex - PS2_AUDIO_STREAM_INDEX_BASE;
- if (musIndex >= ps2->musEntryCount) return -1;
-
- Ps2MusEntry* mus = &ps2->musEntries[musIndex];
-
- // Find a free music stream slot
- Ps2MusicStream* stream = nullptr;
- int streamSlot = -1;
- repeat(MAX_MUSIC_STREAMS, i) {
- if (!ps2->musicStreams[i].active) {
- stream = &ps2->musicStreams[i];
- streamSlot = i;
- break;
- }
- }
-
- if (stream == nullptr) {
- return -1;
- }
-
- int32_t instanceId = PS2_SOUND_INSTANCE_ID_BASE + MAX_PS2_SOUND_INSTANCES + streamSlot;
-
- memset(stream, 0, sizeof(Ps2MusicStream));
- stream->active = true;
- stream->soundIndex = soundIndex;
- stream->audoIndex = -1;
- stream->instanceId = instanceId;
- stream->priority = priority;
- stream->loop = loop;
- stream->paused = false;
- stream->currentGain = 1.0f;
- stream->targetGain = 1.0f;
- stream->startGain = 1.0f;
- stream->sondVolume = 1.0f;
- stream->pitch = 1.0f;
- stream->sondPitch = 1.0f;
-
- stream->fileStartOffset = mus->dataOffset;
- stream->fileEndOffset = mus->dataOffset + mus->dataSize;
- stream->fileOffset = mus->dataOffset;
- stream->decoderPredictor = 0;
- stream->decoderStepIndex = 0;
- stream->endOfTrack = false;
-
- streamFillBuffer(ps2, stream, 0);
- streamFillBuffer(ps2, stream, 1);
- stream->activeBuffer = 0;
- stream->readPosition = 0;
- stream->needsRefill = false;
-
- // fprintf(stderr, "PS2AudioSystem: Streaming MUS '%s', size=%" PRIu32 " bytes, instanceId=%" PRId32 "\n", mus->name, mus->dataSize, instanceId);
-
- return instanceId;
- }
-
- if (0 > soundIndex || (uint16_t) soundIndex >= ps2->sondEntryCount) {
- // fprintf(stderr, "PS2AudioSystem: Invalid sound index %" PRId32 "\n", soundIndex);
- return -1;
- }
-
- Ps2SondEntry* sond = &ps2->sondEntries[soundIndex];
-
- // 0xFFFF = unmapped sound (no audio data)
- if (sond->audoIndex == 0xFFFF) {
- return -1;
- }
-
- if (sond->audoIndex >= ps2->audoEntryCount) {
- // fprintf(stderr, "PS2AudioSystem: Invalid audo index %d for sound %" PRId32 "\n", sond->audoIndex, soundIndex);
- return -1;
- }
-
- // SOND volume and pitch are fixed-point * 256
- // A pitch of 0 means "default" (1.0) in GameMaker
- float sondVolume = (float) sond->volume / 256.0f;
- float sondPitch = (sond->pitch == 0) ? 1.0f : (float) sond->pitch / 256.0f;
-
- bool isEmbedded = (sond->flags & 0x01) != 0;
- bool isCompressed = (sond->flags & 0x02) != 0;
-
- // Some sounds are mis-flagged as embedded when they are actually long tracks (example: "spamton_neo_mix_ex_wip" in DELTARUNE Chapter 2)
- // Decoding them into the LRU cache would blow EE RAM, so force them through the streaming path when the decoded PCM would exceed the cache budget.
- Ps2AudoEntry* audoForSize = &ps2->audoEntries[sond->audoIndex];
- uint32_t decodedPcmBytes = audoForSize->dataSize * 2 * (uint32_t) sizeof(int16_t);
- if ((isEmbedded || isCompressed) && decodedPcmBytes > PS2_SFX_CACHE_MAX_BYTES) {
- fprintf(stderr, "PS2AudioSystem: Sound %" PRId32 " (audo %d) would need %" PRIu32 " bytes of PCM in the cache! isEmbedded? %s; isCompressed? %s; Streaming instead...\n", soundIndex, sond->audoIndex, decodedPcmBytes, isEmbedded ? "true" : "false", isCompressed ? "true" : "false");
- isEmbedded = false;
- isCompressed = false;
- }
-
- if (!isEmbedded && !isCompressed) {
- // ===[ Streaming music path ]===
- // Find a free music stream slot
- Ps2MusicStream* stream = nullptr;
- int streamSlot = -1;
- repeat(MAX_MUSIC_STREAMS, i) {
- if (!ps2->musicStreams[i].active) {
- stream = &ps2->musicStreams[i];
- streamSlot = i;
- break;
- }
- }
-
- if (stream == nullptr) {
- // fprintf(stderr, "PS2AudioSystem: No free music stream slots for sound %" PRId32 "\n", soundIndex);
- return -1;
- }
-
- Ps2AudoEntry* audo = &ps2->audoEntries[sond->audoIndex];
-
- // Use a separate ID range for music streams (offset by MAX_PS2_SOUND_INSTANCES)
- int32_t instanceId = PS2_SOUND_INSTANCE_ID_BASE + MAX_PS2_SOUND_INSTANCES + streamSlot;
-
- memset(stream, 0, sizeof(Ps2MusicStream));
- stream->active = true;
- stream->soundIndex = soundIndex;
- stream->audoIndex = sond->audoIndex;
- stream->instanceId = instanceId;
- stream->priority = priority;
- stream->loop = loop;
- stream->paused = false;
- stream->currentGain = sondVolume;
- stream->targetGain = sondVolume;
- stream->startGain = sondVolume;
- stream->sondVolume = sondVolume;
- stream->pitch = 1.0f;
- stream->sondPitch = sondPitch;
-
- // Set up file streaming state
- stream->fileStartOffset = audo->dataOffset;
- stream->fileEndOffset = audo->dataOffset + audo->dataSize;
- stream->fileOffset = audo->dataOffset;
- stream->decoderPredictor = 0;
- stream->decoderStepIndex = 0;
- stream->endOfTrack = false;
-
- // Fill both buffers initially
- streamFillBuffer(ps2, stream, 0);
- streamFillBuffer(ps2, stream, 1);
- stream->activeBuffer = 0;
- stream->readPosition = 0;
- stream->needsRefill = false;
-
- // fprintf(stderr, "PS2AudioSystem: Streaming music soundIndex=%" PRId32 " audoIndex=%d, size=%" PRIu32 " bytes, instanceId=%" PRId32 "\n", soundIndex, sond->audoIndex, audo->dataSize, instanceId);
-
- return instanceId;
- }
-
- // ===[ Cached SFX path ]===
- // Ensure decoded PCM is in cache
- DecodedPcmEntry* cached = cacheGet(ps2, sond->audoIndex);
- if (cached == nullptr) {
- cached = cacheInsert(ps2, sond->audoIndex);
- if (cached == nullptr) {
- // fprintf(stderr, "PS2AudioSystem: Failed to cache decoded audio for sound %" PRId32 "\n", soundIndex);
- return -1;
- }
- }
-
- // Find a free SFX instance slot
- Ps2SoundInstance* slot = findFreeSlot(ps2);
- if (slot == nullptr) {
- // fprintf(stderr, "PS2AudioSystem: No free sound slots for sound %" PRId32 "\n", soundIndex);
- return -1;
- }
-
- int32_t slotIndex = (int32_t) (slot - ps2->instances);
-
- slot->active = true;
- slot->soundIndex = soundIndex;
- slot->audoIndex = sond->audoIndex;
- slot->instanceId = PS2_SOUND_INSTANCE_ID_BASE + slotIndex;
- slot->priority = priority;
- slot->loop = loop;
- slot->paused = false;
- slot->positionInt = 0;
- slot->positionFrac = 0;
- slot->totalSamples = cached->pcmSampleCount;
- slot->pitch = 1.0f;
- slot->sondPitch = sondPitch;
- slot->currentGain = sondVolume;
- slot->targetGain = sondVolume;
- slot->startGain = sondVolume;
- slot->fadeTimeRemaining = 0.0f;
- slot->fadeTotalTime = 0.0f;
- slot->sondVolume = sondVolume;
-
- ps2->nextInstanceCounter++;
-
- return slot->instanceId;
-}
-
-// ===[ Helper: Apply action to SFX instance, music stream, or all matching by soundIndex ]===
-// These helpers handle the dual SFX/music lookup needed by stop/pause/resume/gain/pitch/etc.
-
-// Find either a SFX instance or music stream by instanceId or soundIndex
-// For instanceId lookups (>= PS2_SOUND_INSTANCE_ID_BASE), returns at most one match.
-// For soundIndex lookups, iterates all matches via callback.
-
-typedef void (*InstanceAction)(Ps2SoundInstance* sfx, Ps2MusicStream* music, void* userData);
-
-static void forEachInstance(Ps2AudioSystem* ps2, int32_t soundOrInstance, InstanceAction action, void* userData) {
- if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) {
- // MUS stream resource index: match by soundIndex on music streams
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) {
- action(nullptr, &ps2->musicStreams[i], userData);
- }
- }
- } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) {
- // Lookup by instance ID
- Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance);
- if (sfx != nullptr) {
- action(sfx, nullptr, userData);
- return;
- }
- Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance);
- if (music != nullptr) {
- action(nullptr, music, userData);
- return;
- }
- } else {
- // Lookup by sound index -- apply to all matching
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- if (ps2->instances[i].active && ps2->instances[i].soundIndex == soundOrInstance) {
- action(&ps2->instances[i], nullptr, userData);
- }
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) {
- action(nullptr, &ps2->musicStreams[i], userData);
- }
- }
- }
-}
-
-static void actionStop(Ps2SoundInstance* sfx, Ps2MusicStream* music, MAYBE_UNUSED void* userData) {
- if (sfx != nullptr) sfx->active = false;
- if (music != nullptr) music->active = false;
-}
-
-static void actionPause(Ps2SoundInstance* sfx, Ps2MusicStream* music, MAYBE_UNUSED void* userData) {
- if (sfx != nullptr) sfx->paused = true;
- if (music != nullptr) music->paused = true;
-}
-
-static void actionResume(Ps2SoundInstance* sfx, Ps2MusicStream* music, MAYBE_UNUSED void* userData) {
- if (sfx != nullptr) sfx->paused = false;
- if (music != nullptr) music->paused = false;
-}
-
-typedef struct {
- float gain;
- uint32_t timeMs;
-} GainParams;
-
-static void actionSetGain(Ps2SoundInstance* sfx, Ps2MusicStream* music, void* userData) {
- GainParams* params = (GainParams*) userData;
- float gain = params->gain;
- uint32_t timeMs = params->timeMs;
-
- if (sfx != nullptr) {
- if (timeMs == 0) {
- sfx->currentGain = gain;
- sfx->targetGain = gain;
- sfx->fadeTimeRemaining = 0.0f;
- } else {
- sfx->startGain = sfx->currentGain;
- sfx->targetGain = gain;
- sfx->fadeTotalTime = (float) timeMs / 1000.0f;
- sfx->fadeTimeRemaining = sfx->fadeTotalTime;
- }
- }
- if (music != nullptr) {
- if (timeMs == 0) {
- music->currentGain = gain;
- music->targetGain = gain;
- music->fadeTimeRemaining = 0.0f;
- } else {
- music->startGain = music->currentGain;
- music->targetGain = gain;
- music->fadeTotalTime = (float) timeMs / 1000.0f;
- music->fadeTimeRemaining = music->fadeTotalTime;
- }
- }
-}
-
-static void actionSetPitch(Ps2SoundInstance* sfx, Ps2MusicStream* music, void* userData) {
- float pitch = *(float*) userData;
- if (sfx != nullptr) sfx->pitch = pitch;
- if (music != nullptr) music->pitch = pitch;
-}
-
-// ===[ Vtable: Stop/Pause/Resume/Gain/Pitch ]===
-
-static void ps2StopSound(AudioSystem* audio, int32_t soundOrInstance) {
- // fprintf(stderr, "PS2AudioSystem: Stopping sound %d\n", soundOrInstance);
- forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionStop, nullptr);
-}
-
-static void ps2StopAll(AudioSystem* audio) {
- // fprintf(stderr, "PS2AudioSystem: Stopping all audios!\n");
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- ps2->instances[i].active = false;
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- ps2->musicStreams[i].active = false;
- }
-}
-
-static bool ps2IsPlaying(AudioSystem* audio, int32_t soundOrInstance) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
-
- if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) {
- // MUS stream resource index: match by soundIndex on music streams
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (stream->active && stream->soundIndex == soundOrInstance && !stream->paused) return true;
- }
- return false;
- } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) {
- Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance);
- if (sfx != nullptr) return !sfx->paused;
- Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance);
- if (music != nullptr) return !music->paused;
- return false;
- } else {
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- Ps2SoundInstance* inst = &ps2->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance && !inst->paused) return true;
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (stream->active && stream->soundIndex == soundOrInstance && !stream->paused) return true;
- }
- return false;
- }
-}
-
-static void ps2PauseSound(AudioSystem* audio, int32_t soundOrInstance) {
- // fprintf(stderr, "PS2AudioSystem: Pausing sound %d\n", soundOrInstance);
- forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionPause, nullptr);
-}
-
-static void ps2ResumeSound(AudioSystem* audio, int32_t soundOrInstance) {
- // fprintf(stderr, "PS2AudioSystem: Resuming sound %d\n", soundOrInstance);
- forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionResume, nullptr);
-}
-
-static void ps2PauseAll(AudioSystem* audio) {
- // fprintf(stderr, "PS2AudioSystem: Pausing all sounds!\n");
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- if (ps2->instances[i].active) ps2->instances[i].paused = true;
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active) ps2->musicStreams[i].paused = true;
- }
-}
-
-static void ps2ResumeAll(AudioSystem* audio) {
- // fprintf(stderr, "PS2AudioSystem: Resuming all sounds!\n");
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- if (ps2->instances[i].active) ps2->instances[i].paused = false;
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active) ps2->musicStreams[i].paused = false;
- }
-}
-
-static void ps2SetSoundGain(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs) {
- GainParams params = { .gain = gain, .timeMs = timeMs };
- forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionSetGain, ¶ms);
-}
-
-static float ps2GetSoundGain(AudioSystem* audio, int32_t soundOrInstance) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) {
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].currentGain;
- }
- } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) {
- Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance);
- if (sfx != nullptr) return sfx->currentGain;
- Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance);
- if (music != nullptr) return music->currentGain;
- } else {
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- if (ps2->instances[i].active && ps2->instances[i].soundIndex == soundOrInstance) return ps2->instances[i].currentGain;
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].currentGain;
- }
- }
- return 0.0f;
-}
-
-static void ps2SetSoundPitch(AudioSystem* audio, int32_t soundOrInstance, float pitch) {
- // fprintf(stderr, "PS2AudioSystem: Setting pitch of sound %d to %f\n", soundOrInstance, pitch);
- forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionSetPitch, &pitch);
-}
-
-static float ps2GetSoundPitch(AudioSystem* audio, int32_t soundOrInstance) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) {
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].pitch;
- }
- } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) {
- Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance);
- if (sfx != nullptr) return sfx->pitch;
- Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance);
- if (music != nullptr) return music->pitch;
- } else {
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- if (ps2->instances[i].active && ps2->instances[i].soundIndex == soundOrInstance) return ps2->instances[i].pitch;
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].pitch;
- }
- }
- return 1.0f;
-}
-
-static float ps2GetTrackPosition(AudioSystem* audio, int32_t soundOrInstance) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
-
- if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) {
- // MUS stream resource index
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (stream->active && stream->soundIndex == soundOrInstance) {
- uint32_t bytesConsumed = stream->fileOffset - stream->fileStartOffset;
- uint32_t samplesConsumed = bytesConsumed * 2;
- return (float) samplesConsumed / (float) getMusicStreamSampleRate(ps2, stream);
- }
- }
- } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) {
- Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance);
- if (sfx != nullptr && sfx->audoIndex < ps2->audoEntryCount) {
- return (float) sfx->positionInt / (float) ps2->audoEntries[sfx->audoIndex].sampleRate;
- }
- Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance);
- if (music != nullptr) {
- uint32_t bytesConsumed = music->fileOffset - music->fileStartOffset;
- uint32_t samplesConsumed = bytesConsumed * 2;
- return (float) samplesConsumed / (float) getMusicStreamSampleRate(ps2, music);
- }
- } else {
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- Ps2SoundInstance* inst = &ps2->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance && inst->audoIndex < ps2->audoEntryCount) {
- return (float) inst->positionInt / (float) ps2->audoEntries[inst->audoIndex].sampleRate;
- }
- }
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (stream->active && stream->soundIndex == soundOrInstance) {
- uint32_t bytesConsumed = stream->fileOffset - stream->fileStartOffset;
- uint32_t samplesConsumed = bytesConsumed * 2;
- return (float) samplesConsumed / (float) getMusicStreamSampleRate(ps2, stream);
- }
- }
- }
- return 0.0f;
-}
-
-// Seek a music stream to a position in seconds
-static void seekMusicStream(Ps2AudioSystem* ps2, Ps2MusicStream* music, float positionSeconds) {
- float sampleRate = (float) getMusicStreamSampleRate(ps2, music);
- uint32_t targetSample = (uint32_t) (positionSeconds * sampleRate);
- // Convert sample position to ADPCM byte offset (2 samples per byte)
- uint32_t byteOffset = targetSample / 2;
- uint32_t maxBytes = music->fileEndOffset - music->fileStartOffset;
- if (byteOffset > maxBytes) byteOffset = maxBytes;
-
- // Reset decoder state and seek
- music->fileOffset = music->fileStartOffset + byteOffset;
- music->decoderPredictor = 0;
- music->decoderStepIndex = 0;
- music->endOfTrack = false;
- music->readPosition = 0;
-
- // Re-fill both buffers from the new position
- streamFillBuffer(ps2, music, 0);
- streamFillBuffer(ps2, music, 1);
- music->activeBuffer = 0;
- music->needsRefill = false;
-}
-
-static void ps2SetTrackPosition(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds) {
- // fprintf(stderr, "PS2AudioSystem: Setting track position of sound %d to %f\n", soundOrInstance, positionSeconds);
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
-
- if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) {
- // MUS stream resource index
- repeat(MAX_MUSIC_STREAMS, i) {
- Ps2MusicStream* stream = &ps2->musicStreams[i];
- if (stream->active && stream->soundIndex == soundOrInstance) {
- seekMusicStream(ps2, stream, positionSeconds);
- return;
- }
- }
- } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) {
- // SFX track position
- Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance);
- if (sfx != nullptr && sfx->audoIndex < ps2->audoEntryCount) {
- float sampleRate = (float) ps2->audoEntries[sfx->audoIndex].sampleRate;
- sfx->positionInt = (uint32_t) (positionSeconds * sampleRate);
- sfx->positionFrac = 0;
- if (sfx->positionInt >= sfx->totalSamples) {
- sfx->positionInt = sfx->totalSamples > 0 ? sfx->totalSamples - 1 : 0;
- }
- return;
- }
- // Music stream seek: reset decoder and re-seek
- Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance);
- if (music != nullptr) {
- seekMusicStream(ps2, music, positionSeconds);
- }
- } else {
- // By sound index (apply to first match)
- repeat(MAX_PS2_SOUND_INSTANCES, i) {
- Ps2SoundInstance* inst = &ps2->instances[i];
- if (inst->active && inst->soundIndex == soundOrInstance && inst->audoIndex < ps2->audoEntryCount) {
- float sampleRate = (float) ps2->audoEntries[inst->audoIndex].sampleRate;
- inst->positionInt = (uint32_t) (positionSeconds * sampleRate);
- inst->positionFrac = 0;
- if (inst->positionInt >= inst->totalSamples) {
- inst->positionInt = inst->totalSamples > 0 ? inst->totalSamples - 1 : 0;
- }
- return;
- }
- }
- }
-}
-
-// Total length of a sound in seconds. Looks up the AUDO or MUS entry's decoded sampleCount and divides by sampleRate.
-static float ps2GetSoundLength(AudioSystem* audio, int32_t soundOrInstance) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
-
- int32_t audoIndex = -1;
- int32_t musIndex = -1;
-
- if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) {
- musIndex = soundOrInstance - PS2_AUDIO_STREAM_INDEX_BASE;
- } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) {
- Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance);
- if (sfx != nullptr) {
- audoIndex = sfx->audoIndex;
- } else {
- Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance);
- if (music != nullptr && music->soundIndex >= PS2_AUDIO_STREAM_INDEX_BASE) {
- musIndex = music->soundIndex - PS2_AUDIO_STREAM_INDEX_BASE;
- }
- }
- } else {
- // SOND resource index — map to AUDO via the SOND entry
- if (ps2->sondEntryCount > (uint32_t) soundOrInstance) {
- uint16_t mapped = ps2->sondEntries[soundOrInstance].audoIndex;
- if (mapped != 0xFFFF) audoIndex = mapped;
- }
- }
-
- if (ps2->audoEntryCount > (uint32_t) audoIndex) {
- if (audoIndex >= 0) {
- Ps2AudoEntry* audo = &ps2->audoEntries[audoIndex];
- if (audo->sampleRate == 0 || audo->sampleCount == 0) return 0.0f;
- return (float) audo->sampleCount / (float) audo->sampleRate;
- }
- if (musIndex >= 0) {
- Ps2MusEntry* mus = &ps2->musEntries[musIndex];
- if (mus->sampleRate == 0 || mus->sampleCount == 0) return 0.0f;
- return (float) mus->sampleCount / (float) mus->sampleRate;
- }
- }
- return 0.0f;
-}
-
-static void ps2SetMasterGain(AudioSystem* audio, float gain) {
- // fprintf(stderr, "PS2AudioSystem: Setting master gain to %f\n", gain);
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- ps2->masterGain = gain;
-}
-
-static void ps2SetChannelCount(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t count) {
- // No-op: software mixer handles all channels internally
-}
-
-static void ps2GroupLoad(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t groupIndex) {
- // No-op: all audio is available from SOUNDS.BIN
-}
-
-static bool ps2GroupIsLoaded(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t groupIndex) {
- return true;
-}
-
-static int32_t ps2CreateStream(AudioSystem* audio, const char* filename) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
- if (!ps2->initialized) return -1;
-
- // Look up the filename in the MUS string table
- for (int i = 0; ps2->musEntryCount > i; i++) {
- if (strcmp(ps2->musEntries[i].name, filename) == 0) {
- int32_t streamIndex = PS2_AUDIO_STREAM_INDEX_BASE + i;
- fprintf(stderr, "PS2AudioSystem: Created stream %" PRId32 " for '%s'\n", streamIndex, filename);
- return streamIndex;
- }
- }
-
- fprintf(stderr, "PS2AudioSystem: audio_create_stream: '%s' not found in MUS entries\n", filename);
- return -1;
-}
-
-static bool ps2DestroyStream(AudioSystem* audio, int32_t streamIndex) {
- Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio;
-
- // Stop all music streams that were playing this stream
- repeat(MAX_MUSIC_STREAMS, i) {
- if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == streamIndex) {
- ps2->musicStreams[i].active = false;
- }
- }
-
- return true;
-}
-
-// ===[ Vtable ]===
-
-static AudioSystemVtable ps2AudioSystemVtable = {
- .init = ps2Init,
- .destroy = ps2Destroy,
- .update = ps2Update,
- .playSound = ps2PlaySound,
- .stopSound = ps2StopSound,
- .stopAll = ps2StopAll,
- .isPlaying = ps2IsPlaying,
- .pauseSound = ps2PauseSound,
- .resumeSound = ps2ResumeSound,
- .pauseAll = ps2PauseAll,
- .resumeAll = ps2ResumeAll,
- .setSoundGain = ps2SetSoundGain,
- .getSoundGain = ps2GetSoundGain,
- .setSoundPitch = ps2SetSoundPitch,
- .getSoundPitch = ps2GetSoundPitch,
- .getTrackPosition = ps2GetTrackPosition,
- .setTrackPosition = ps2SetTrackPosition,
- .getSoundLength = ps2GetSoundLength,
- .setMasterGain = ps2SetMasterGain,
- .setChannelCount = ps2SetChannelCount,
- .groupLoad = ps2GroupLoad,
- .groupIsLoaded = ps2GroupIsLoaded,
- .createStream = ps2CreateStream,
- .destroyStream = ps2DestroyStream,
-};
-
-// ===[ Lifecycle ]===
-
-Ps2AudioSystem* Ps2AudioSystem_create(void) {
- Ps2AudioSystem* ps2 = safeCalloc(1, sizeof(Ps2AudioSystem));
- ps2->base.vtable = &ps2AudioSystemVtable;
- ps2->masterGain = 1.0f;
- return ps2;
-}
diff --git a/src/ps2/ps2_audio_system.h b/src/ps2/ps2_audio_system.h
deleted file mode 100644
index bc9545f4..00000000
--- a/src/ps2/ps2_audio_system.h
+++ /dev/null
@@ -1,175 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "../audio_system.h"
-
-#include
-#include
-#include
-
-#define MAX_PS2_SOUND_INSTANCES 64
-#define PS2_SOUND_INSTANCE_ID_BASE 100000
-#define LRU_CACHE_SIZE 64
-#define MIX_BUFFER_SAMPLES 512
-#define AUDSRV_OUTPUT_FREQ 22050
-
-// Streaming music: decode ADPCM in chunks, double-buffered
-// Each buffer holds STREAM_DECODE_SAMPLES decoded PCM samples
-#define MAX_MUSIC_STREAMS 4
-#define PS2_AUDIO_STREAM_INDEX_BASE 300000
-#define STREAM_ADPCM_CHUNK_BYTES 4096
-#define STREAM_DECODE_SAMPLES (STREAM_ADPCM_CHUNK_BYTES * 2) // 2 samples per ADPCM byte
-
-// Maximum decoded PCM size for a single cached SFX. Anything larger is routed through the streaming path.
-#define PS2_SFX_CACHE_MAX_BYTES (512 * 1024)
-
-// ===[ SOUNDBNK.BIN Structs ]===
-
-typedef struct {
- uint16_t audoIndex; // index into AUDO table, 0xFFFF = unmapped
- uint32_t flags;
- int16_t volume; // fixed-point: original float * 256
- int16_t pitch; // fixed-point: original float * 256
-} Ps2SondEntry;
-
-typedef struct {
- uint32_t dataOffset; // byte offset in SOUNDS.BIN
- uint32_t dataSize; // bytes in SOUNDS.BIN
- uint16_t sampleRate;
- uint8_t channels;
- uint8_t bitsPerSample;
- uint8_t format; // 0=PCM, 1=IMA ADPCM
- uint32_t sampleCount; // decoded samples per channel; length in seconds = sampleCount / sampleRate
-} Ps2AudoEntry;
-
-// ===[ SOUNDBNK.BIN MUS (Streamed Music) Structs ]===
-
-#define MAX_MUS_ENTRIES 256
-
-typedef struct {
- char* name; // path string (e.g. "mus/field_of_hopes.ogg")
- uint32_t dataOffset; // byte offset in SOUNDS.BIN
- uint32_t dataSize; // bytes in SOUNDS.BIN
- uint16_t sampleRate;
- uint8_t channels;
- uint8_t format; // 0=PCM, 1=IMA ADPCM
- uint32_t sampleCount; // decoded samples per channel; length in seconds = sampleCount / sampleRate
-} Ps2MusEntry;
-
-// ===[ LRU Decoded PCM Cache ]===
-
-typedef struct {
- int32_t audoIndex; // -1 = empty slot
- int16_t* pcmData;
- uint32_t pcmSampleCount; // number of mono samples
- uint32_t pcmDataBytes;
- uint32_t lastAccessCounter;
-} DecodedPcmEntry;
-
-// ===[ Sound Instance ]===
-
-typedef struct {
- bool active;
- int32_t soundIndex; // SOND resource index
- int32_t audoIndex; // AUDO resource index
- int32_t instanceId; // unique ID returned to GML
- int32_t priority;
- bool loop;
- bool paused;
-
- // Playback position (32.32 fixed-point for fractional sample stepping)
- uint32_t positionInt;
- uint32_t positionFrac;
- uint32_t totalSamples;
-
- // Pitch
- float pitch; // runtime pitch set by GML
- float sondPitch; // SOND resource pitch (fixed-point / 256)
-
- // Gain / volume
- float currentGain;
- float targetGain;
- float startGain;
- float fadeTimeRemaining;
- float fadeTotalTime;
- float sondVolume; // SOND resource volume (fixed-point / 256)
-} Ps2SoundInstance;
-
-// ===[ Streaming Music Instance ]===
-
-typedef struct {
- bool active;
- int32_t soundIndex;
- int32_t audoIndex;
- int32_t instanceId;
- int32_t priority;
- bool loop;
- bool paused;
-
- // Gain / volume (same fields as Ps2SoundInstance)
- float currentGain;
- float targetGain;
- float startGain;
- float fadeTimeRemaining;
- float fadeTotalTime;
- float sondVolume;
- float pitch;
- float sondPitch;
-
- // ADPCM file streaming state
- uint32_t fileOffset; // current read position in SOUNDS.BIN
- uint32_t fileStartOffset; // start offset of this track in SOUNDS.BIN
- uint32_t fileEndOffset; // end offset (fileStartOffset + dataSize)
-
- // IMA ADPCM decoder state (persists across chunks)
- int32_t decoderPredictor;
- int32_t decoderStepIndex;
-
- // Double-buffered decoded PCM
- int16_t buffers[2][STREAM_DECODE_SAMPLES];
- uint32_t bufferSampleCount[2]; // actual samples decoded in each buffer (may be < STREAM_DECODE_SAMPLES at end of track)
- int activeBuffer; // which buffer the mixer is reading from (0 or 1)
- uint32_t readPosition; // integer sample position within the active buffer
- uint32_t readPositionFrac; // fractional part (32-bit) for pitch resampling
- bool needsRefill; // true when the back buffer needs to be filled
- bool endOfTrack; // true when we've read all ADPCM data
-} Ps2MusicStream;
-
-// ===[ PS2 Audio System ]===
-
-typedef struct {
- AudioSystem base;
-
- // SOUNDBNK.BIN index
- uint16_t sondEntryCount;
- uint16_t audoEntryCount;
- uint16_t musEntryCount;
- Ps2SondEntry* sondEntries;
- Ps2AudoEntry* audoEntries;
- Ps2MusEntry* musEntries;
-
- // SOUNDS.BIN file handle (streamed on demand, not loaded into RAM)
- FILE* soundsFile;
-
- // LRU decoded PCM cache (for short embedded SFX)
- DecodedPcmEntry cacheEntries[LRU_CACHE_SIZE];
- uint32_t cacheAccessCounter;
-
- // SFX instance slots (embedded sounds, fully decoded in LRU cache)
- Ps2SoundInstance instances[MAX_PS2_SOUND_INSTANCES];
- int32_t nextInstanceCounter;
-
- // Streaming music slots (non-embedded sounds, double-buffered from disc)
- Ps2MusicStream musicStreams[MAX_MUSIC_STREAMS];
-
- // Mixer output buffer (stereo interleaved)
- int16_t mixBuffer[MIX_BUFFER_SAMPLES * 2];
-
- // Mixer accumulator (int32 mono; duplicated to L/R at clamp step)
- int32_t mixAccum[MIX_BUFFER_SAMPLES];
-
- float masterGain;
- bool initialized;
-} Ps2AudioSystem;
-
-Ps2AudioSystem* Ps2AudioSystem_create(void);
diff --git a/src/ps2/ps2_file_system.c b/src/ps2/ps2_file_system.c
deleted file mode 100644
index 62b7023a..00000000
--- a/src/ps2/ps2_file_system.c
+++ /dev/null
@@ -1,515 +0,0 @@
-#include "ps2_file_system.h"
-#include "ps2_utils.h"
-#include "../json_reader.h"
-#include "../utils.h"
-
-#include
-#include
-#include
-#include
-
-#include "stb_ds.h"
-
-// ===[ Internal Types ]===
-
-typedef struct {
- char* key; // game-relative file name
- char** value; // stb_ds dynamic array of resolved device paths
-} Ps2FileMapping;
-
-// Parsed save icon configuration from CONFIG.JSN "saveIcon" section
-typedef struct {
- uint32_t bgAlpha;
- int32_t bgColors[4][4]; // 4 corners x RGBA
- float lightDirs[3][4]; // 3 lights x XYZW
- float lightColors[3][4]; // 3 lights x RGBA
- float ambient[4]; // RGBA
-} SaveIconConfig;
-
-typedef struct {
- FileSystem base;
- Ps2FileMapping* mappings; // stb_ds string hashmap
- char* gameTitle; // game display name for icon.sys generation
- SaveIconConfig saveIconConfig;
-} Ps2FileSystem;
-
-// ===[ Helpers ]===
-
-// Expands $BOOT: prefix to the boot device path, or returns a strdup of the input
-static char* expandBootPrefix(const char* path) {
- const char* bootPrefix = "$BOOT:";
- size_t bootPrefixLen = strlen(bootPrefix);
-
- if (strncmp(path, bootPrefix, bootPrefixLen) == 0) {
- const char* relativePart = path + bootPrefixLen;
- return PS2Utils_createDevicePath(relativePart);
- }
-
- return safeStrdup(path);
-}
-
-// ===[ icon.sys Generation ]===
-// icon.sys is a fixed 964-byte file required by the PS2 memory card browser
-// Without it, the save directory shows as "Corrupted Data"
-
-#define ICON_SYS_SIZE 964
-
-// Converts an ASCII character to its full-width Shift-JIS encoding (2 bytes)
-// The PS2 memory card browser only renders Shift-JIS glyphs, not plain ASCII
-// If a character is not supported, it will fall back to a space character
-static void asciiToShiftJIS(char c, uint8_t* out) {
- if (c == ' ') {
- out[0] = 0x81; out[1] = 0x40;
- } else if (c >= '0' && '9' >= c) {
- out[0] = 0x82; out[1] = 0x4F + (c - '0');
- } else if (c >= 'A' && 'Z' >= c) {
- out[0] = 0x82; out[1] = 0x60 + (c - 'A');
- } else if (c >= 'a' && 'z' >= c) {
- out[0] = 0x82; out[1] = 0x81 + (c - 'a');
- } else if (c == '!') {
- out[0] = 0x81; out[1] = 0x49;
- } else if (c == '?') {
- out[0] = 0x81; out[1] = 0x48;
- } else if (c == '.') {
- out[0] = 0x81; out[1] = 0x44;
- } else if (c == ',') {
- out[0] = 0x81; out[1] = 0x43;
- } else if (c == ':') {
- out[0] = 0x81; out[1] = 0x46;
- } else if (c == '-') {
- out[0] = 0x81; out[1] = 0x7C;
- } else if (c == '(') {
- out[0] = 0x81; out[1] = 0x69;
- } else if (c == ')') {
- out[0] = 0x81; out[1] = 0x6A;
- } else {
- // Unsupported character, use full-width space as fallback
- out[0] = 0x81; out[1] = 0x40;
- }
-}
-
-// Writes the game title as full-width Shift-JIS into the icon.sys title field (68 bytes max)
-static void writeShiftJISTitle(uint8_t* titleField, const char* gameTitle) {
- size_t srcLen = strlen(gameTitle);
- size_t dstPos = 0;
- size_t maxBytes = 66; // 68 bytes minus 2 for null terminator safety
-
- repeat(srcLen, i) {
- if (dstPos + 2 > maxBytes)
- break;
- asciiToShiftJIS(gameTitle[i], titleField + dstPos);
- dstPos += 2; // Each Shift-JIS character is 2 bytes
- }
-}
-
-static void generateIconSys(uint8_t* buffer, const char* gameTitle, const SaveIconConfig* config) {
- memset(buffer, 0, ICON_SYS_SIZE);
-
- // Magic "PS2D"
- memcpy(buffer + 0x000, "PS2D", 4);
-
- // Offset 4: 2 bytes reserved (0)
- // Offset 6: 2 bytes newline offset in title (0 = no line break)
- // Offset 8: 4 bytes reserved (0)
- // All left as 0 from memset
-
- // Background transparency (0x00-0x80)
- memcpy(buffer + 0x00C, &config->bgAlpha, 4);
-
- // Background colors: 4 corners x RGBA as int32 (0-255 range)
- memcpy(buffer + 0x010, config->bgColors, sizeof(config->bgColors));
-
- // Light directions: 3 lights x XYZW as float
- memcpy(buffer + 0x050, config->lightDirs, sizeof(config->lightDirs));
-
- // Light colors: 3 lights x RGBA as float (0.0-1.0)
- memcpy(buffer + 0x080, config->lightColors, sizeof(config->lightColors));
-
- // Ambient color: RGBA as float
- memcpy(buffer + 0x0B0, config->ambient, sizeof(config->ambient));
-
- // Title (68 bytes, full-width Shift-JIS encoded)
- writeShiftJISTitle(buffer + 0x0C0, gameTitle);
-
- // Icon filenames (64 bytes each) at 0x104, 0x144, 0x184
- // All three (normal, copy, delete) reference the same icon file
- const char* iconFileName = "ICON.ICO";
- size_t iconNameLen = strlen(iconFileName);
- memcpy(buffer + 0x104, iconFileName, iconNameLen);
- memcpy(buffer + 0x144, iconFileName, iconNameLen);
- memcpy(buffer + 0x184, iconFileName, iconNameLen);
-}
-
-// Copies a file from src to dst (binary). Returns true on success.
-static bool copyFile(const char* srcPath, const char* dstPath) {
- FILE* src = fopen(srcPath, "rb");
- if (src == nullptr)
- return false;
-
- fseek(src, 0, SEEK_END);
- long size = ftell(src);
- fseek(src, 0, SEEK_SET);
-
- uint8_t* data = safeMalloc((size_t) size);
- size_t bytesRead = fread(data, 1, (size_t) size, src);
- fclose(src);
-
- FILE* dst = fopen(dstPath, "wb");
- if (dst == nullptr) {
- free(data);
- return false;
- }
-
- size_t written = fwrite(data, 1, bytesRead, dst);
- fclose(dst);
- free(data);
- return written == bytesRead;
-}
-
-// Copies ICON.ICO from the boot device into the given directory if it doesn't already exist
-static void copyIconIcoIfMissing(const char* dirPath) {
- size_t dirLen = strlen(dirPath);
- size_t pathLen = dirLen + 1 + 8 + 1; // "/ICON.ICO\0"
- char* dstPath = safeMalloc(pathLen);
- snprintf(dstPath, pathLen, "%s/ICON.ICO", dirPath);
-
- // Check if it already exists on the memory card
- FILE* check = fopen(dstPath, "rb");
- if (check != nullptr) {
- fclose(check);
- free(dstPath);
- return;
- }
-
- // Copy from boot device
- char* srcPath = PS2Utils_createDevicePath("ICON.ICO");
- if (copyFile(srcPath, dstPath)) {
- fprintf(stderr, "Ps2FileSystem: Copied ICON.ICO to %s\n", dirPath);
- } else {
- fprintf(stderr, "Ps2FileSystem: Failed to copy ICON.ICO from %s to %s\n", srcPath, dstPath);
- }
-
- free(srcPath);
- free(dstPath);
-}
-
-// Writes icon.sys into the given directory if it doesn't already exist
-static void writeIconSysIfMissing(const char* dirPath, const char* gameTitle, const SaveIconConfig* config) {
- // Build path: "dirPath/icon.sys"
- size_t dirLen = strlen(dirPath);
- size_t pathLen = dirLen + 1 + 8 + 1; // "/icon.sys\0"
- char* iconSysPath = safeMalloc(pathLen);
- snprintf(iconSysPath, pathLen, "%s/icon.sys", dirPath);
-
- // Check if it already exists
- FILE* check = fopen(iconSysPath, "rb");
- if (check != nullptr) {
- fclose(check);
- free(iconSysPath);
- return;
- }
-
- // Generate and write
- uint8_t buffer[ICON_SYS_SIZE];
- generateIconSys(buffer, gameTitle, config);
-
- FILE* f = fopen(iconSysPath, "wb");
- if (f != nullptr) {
- fwrite(buffer, 1, ICON_SYS_SIZE, f);
- fclose(f);
- fprintf(stderr, "Ps2FileSystem: Created icon.sys in %s\n", dirPath);
- } else {
- fprintf(stderr, "Ps2FileSystem: Failed to create icon.sys in %s\n", dirPath);
- }
-
- free(iconSysPath);
-}
-
-// Ensures the parent directory exists for mc0:/mc1: paths by calling mkdir
-// Also writes icon.sys and copies ICON.ICO if the directory is newly created
-static void ensureParentDirectory(Ps2FileSystem* pfs, const char* path) {
- // Only do this for memory card paths
- if (strncmp(path, "mc0:", 4) != 0 && strncmp(path, "mc1:", 4) != 0)
- return;
-
- char* pathCopy = safeStrdup(path);
- char* lastSlash = strrchr(pathCopy, '/');
- if (lastSlash != nullptr && lastSlash != pathCopy) {
- *lastSlash = '\0';
- mkdir(pathCopy, 0777);
- writeIconSysIfMissing(pathCopy, pfs->gameTitle, &pfs->saveIconConfig);
- copyIconIcoIfMissing(pathCopy);
- }
- free(pathCopy);
-}
-
-// ===[ Vtable Implementations ]===
-
-static char* resolvePath(FileSystem* fs, const char* relativePath) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- ptrdiff_t idx = shgeti(pfs->mappings, relativePath);
- if (0 > idx)
- return nullptr;
-
- // Return the first mapped path
- if (arrlen(pfs->mappings[idx].value) > 0)
- return safeStrdup(pfs->mappings[idx].value[0]);
-
- return nullptr;
-}
-
-static bool fileExists(FileSystem* fs, const char* relativePath) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- ptrdiff_t idx = shgeti(pfs->mappings, relativePath);
- if (0 > idx)
- return false;
-
- char** paths = pfs->mappings[idx].value;
- int pathCount = arrlen(paths);
- repeat(pathCount, i) {
- FILE* f = fopen(paths[i], "rb");
- if (f != nullptr) {
- fclose(f);
- return true;
- }
- }
-
- return false;
-}
-
-static char* readFileText(FileSystem* fs, const char* relativePath) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- ptrdiff_t idx = shgeti(pfs->mappings, relativePath);
- if (0 > idx)
- return nullptr;
-
- // For the PlayStation 2 target, we have multiple "search" paths for a specific file
- // The reason why we do this is because GameMaker allows files to be in two different folders: The save folder and the bundled folder
- // However, hitting the memory card for some specific files that ARE NOT in the memory card is a bit expensive
- char** paths = pfs->mappings[idx].value;
- int pathCount = arrlen(paths);
- repeat(pathCount, i) {
- FILE* f = fopen(paths[i], "rb");
- if (f == nullptr)
- continue;
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- char* content = safeMalloc((size_t) size + 1);
- size_t bytesRead = fread(content, 1, (size_t) size, f);
- content[bytesRead] = '\0';
- fclose(f);
- return content;
- }
-
- return nullptr;
-}
-
-static bool writeFileText(FileSystem* fs, const char* relativePath, const char* contents) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- ptrdiff_t idx = shgeti(pfs->mappings, relativePath);
- if (0 > idx)
- return false;
-
- char** paths = pfs->mappings[idx].value;
- if (arrlen(paths) == 0)
- return false;
-
- // Write to the first path (the first path is ALWAYS the writeable path)
- const char* writePath = paths[0];
- ensureParentDirectory(pfs, writePath);
-
- FILE* f = fopen(writePath, "wb");
- if (f == nullptr)
- return false;
-
- size_t len = strlen(contents);
- size_t written = fwrite(contents, 1, len, f);
- fclose(f);
- return written == len;
-}
-
-static bool deleteFile(FileSystem* fs, const char* relativePath) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- ptrdiff_t idx = shgeti(pfs->mappings, relativePath);
- if (0 > idx)
- return false;
-
- char** paths = pfs->mappings[idx].value;
- if (arrlen(paths) == 0)
- return false;
-
- // Delete the first path
- return remove(paths[0]) == 0;
-}
-
-static bool ps2ReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- ptrdiff_t idx = shgeti(pfs->mappings, relativePath);
- if (0 > idx)
- return false;
-
- char** paths = pfs->mappings[idx].value;
- int pathCount = arrlen(paths);
- repeat(pathCount, i) {
- FILE* f = fopen(paths[i], "rb");
- if (f == nullptr)
- continue;
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- uint8_t* data = safeMalloc((size_t) size);
- size_t bytesRead = fread(data, 1, (size_t) size, f);
- fclose(f);
-
- *outData = data;
- *outSize = (int32_t) bytesRead;
- return true;
- }
-
- return false;
-}
-
-static bool ps2WriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- ptrdiff_t idx = shgeti(pfs->mappings, relativePath);
- if (0 > idx)
- return false;
-
- char** paths = pfs->mappings[idx].value;
- if (arrlen(paths) == 0)
- return false;
-
- const char* writePath = paths[0];
- ensureParentDirectory(pfs, writePath);
-
- FILE* f = fopen(writePath, "wb");
- if (f == nullptr)
- return false;
-
- size_t written = fwrite(data, 1, (size_t) size, f);
- fclose(f);
- return written == (size_t) size;
-}
-
-// ===[ Vtable ]===
-
-static FileSystemVtable ps2FileSystemVtable = {
- .resolvePath = resolvePath,
- .fileExists = fileExists,
- .readFileText = readFileText,
- .writeFileText = writeFileText,
- .deleteFile = deleteFile,
- .readFileBinary = ps2ReadFileBinary,
- .writeFileBinary = ps2WriteFileBinary,
-};
-
-// ===[ Lifecycle ]===
-
-static SaveIconConfig parseSaveIconConfig(JsonValue* configRoot) {
- JsonValue* saveIconObj = JsonReader_getObject(configRoot, "saveIcon");
- requireNotNullMessage(saveIconObj, "CONFIG.JSN is missing the 'saveIcon' section");
- require(JsonReader_isObject(saveIconObj));
-
- SaveIconConfig config = {0};
-
- // bgAlpha (0x00-0x80)
- JsonValue* bgAlphaVal = JsonReader_getObject(saveIconObj, "bgAlpha");
- requireNotNullMessage(bgAlphaVal, "saveIcon.bgAlpha is missing");
- config.bgAlpha = (uint32_t) JsonReader_getDouble(bgAlphaVal);
-
- // bgColors: array of 4 arrays of 3 ints [R, G, B] (A is always 0)
- JsonValue* bgColorsArr = JsonReader_getObject(saveIconObj, "bgColors");
- requireNotNullMessage(bgColorsArr, "saveIcon.bgColors is missing");
- require(JsonReader_isArray(bgColorsArr) && JsonReader_arrayLength(bgColorsArr) == 4);
- repeat(4, i) {
- JsonValue* corner = JsonReader_getArrayElement(bgColorsArr, i);
- JsonReader_readInt32Array(corner, config.bgColors[i], 3);
- config.bgColors[i][3] = 0; // A = 0
- }
-
- // lightDirs: array of 3 arrays of 3 floats [X, Y, Z] (W is always 0.0)
- JsonValue* lightDirsArr = JsonReader_getObject(saveIconObj, "lightDirs");
- requireNotNullMessage(lightDirsArr, "saveIcon.lightDirs is missing");
- require(JsonReader_isArray(lightDirsArr) && JsonReader_arrayLength(lightDirsArr) == 3);
- repeat(3, i) {
- JsonValue* dir = JsonReader_getArrayElement(lightDirsArr, i);
- JsonReader_readFloatArray(dir, config.lightDirs[i], 3);
- config.lightDirs[i][3] = 0.0f; // W = 0
- }
-
- // lightColors: array of 3 arrays of 3 floats [R, G, B] (A is always 0.0)
- JsonValue* lightColorsArr = JsonReader_getObject(saveIconObj, "lightColors");
- requireNotNullMessage(lightColorsArr, "saveIcon.lightColors is missing");
- require(JsonReader_isArray(lightColorsArr) && JsonReader_arrayLength(lightColorsArr) == 3);
- repeat(3, i) {
- JsonValue* color = JsonReader_getArrayElement(lightColorsArr, i);
- JsonReader_readFloatArray(color, config.lightColors[i], 3);
- config.lightColors[i][3] = 0.0f; // A = 0
- }
-
- // ambient: array of 3 floats [R, G, B] (A is always 0.0)
- JsonValue* ambientArr = JsonReader_getObject(saveIconObj, "ambient");
- requireNotNullMessage(ambientArr, "saveIcon.ambient is missing");
- JsonReader_readFloatArray(ambientArr, config.ambient, 3);
- config.ambient[3] = 0.0f; // A = 0
-
- return config;
-}
-
-FileSystem* Ps2FileSystem_create(JsonValue* configRoot, const char* gameTitle) {
- JsonValue* fileSystemObj = JsonReader_getObject(configRoot, "fileSystem");
- require(fileSystemObj != nullptr && JsonReader_isObject(fileSystemObj));
-
- Ps2FileSystem* pfs = safeCalloc(1, sizeof(Ps2FileSystem));
- pfs->base.vtable = &ps2FileSystemVtable;
- pfs->gameTitle = safeStrdup(gameTitle);
- pfs->saveIconConfig = parseSaveIconConfig(configRoot);
- pfs->mappings = nullptr;
- sh_new_strdup(pfs->mappings);
-
- int entryCount = JsonReader_objectLength(fileSystemObj);
- repeat(entryCount, i) {
- const char* gameFileName = JsonReader_getObjectKey(fileSystemObj, i);
- JsonValue* pathArray = JsonReader_getObjectValue(fileSystemObj, i);
-
- require(JsonReader_isArray(pathArray));
-
- char** resolvedPaths = nullptr;
- int pathCount = JsonReader_arrayLength(pathArray);
- repeat(pathCount, j) {
- JsonValue* pathElement = JsonReader_getArrayElement(pathArray, j);
- require(JsonReader_isString(pathElement));
-
- const char* rawPath = JsonReader_getString(pathElement);
- char* resolved = expandBootPrefix(rawPath);
- arrput(resolvedPaths, resolved);
- fprintf(stderr, "Ps2FileSystem: '%s' -> '%s'\n", gameFileName, resolved);
- }
-
- shput(pfs->mappings, gameFileName, resolvedPaths);
- }
-
- fprintf(stderr, "Ps2FileSystem: Loaded %d file mappings\n", (int) shlen(pfs->mappings));
- return (FileSystem*) pfs;
-}
-
-void Ps2FileSystem_destroy(FileSystem* fs) {
- Ps2FileSystem* pfs = (Ps2FileSystem*) fs;
- free(pfs->gameTitle);
- int mappingCount = shlen(pfs->mappings);
- repeat(mappingCount, i) {
- char** paths = pfs->mappings[i].value;
- int pathCount = arrlen(paths);
- repeat(pathCount, j) {
- free(paths[j]);
- }
- arrfree(paths);
- }
- shfree(pfs->mappings);
- free(pfs);
-}
diff --git a/src/ps2/ps2_file_system.h b/src/ps2/ps2_file_system.h
deleted file mode 100644
index 25a977ea..00000000
--- a/src/ps2/ps2_file_system.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-
-#include "common.h"
-#include "../file_system.h"
-#include "../json_reader.h"
-
-// Creates a PS2 file system that maps game-relative file names to PS2 device paths
-// using the "fileSystem" object from a parsed CONFIG.JSN root
-//
-// configRoot: parsed JSON root of CONFIG.JSN (caller retains ownership, not freed here)
-// gameTitle: the game's display name (used for icon.sys on memory card saves)
-FileSystem* Ps2FileSystem_create(JsonValue* configRoot, const char* gameTitle);
-void Ps2FileSystem_destroy(FileSystem* fs);
diff --git a/src/ps2/ps2_utils.c b/src/ps2/ps2_utils.c
deleted file mode 100644
index 39ac083d..00000000
--- a/src/ps2/ps2_utils.c
+++ /dev/null
@@ -1,118 +0,0 @@
-#include
-#include
-#include
-#include