diff --git a/.github/workflows/macos-platform.yml b/.github/workflows/macos-platform.yml new file mode 100644 index 000000000..657ca649e --- /dev/null +++ b/.github/workflows/macos-platform.yml @@ -0,0 +1,141 @@ +name: macOS platform + +on: + push: + tags: ['v*'] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + macos-posix: + # Apple Silicon runner: verify the native arm64 game and runtime target. + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build configuration test + run: | + python -I -B tests/test_macos_build_path.py + python -I third_party/a64-kit/lift/build_emit.py + python -I scripts/macos/check-arm64-translation.py + + - name: Link native arm64 macOS executable + run: | + printf 'int main(void) { return 0; }\n' > "$RUNNER_TEMP/macos-probe.c" + clang -target arm64-apple-macos13.3 -mcpu=apple-m1 "$RUNNER_TEMP/macos-probe.c" -o "$RUNNER_TEMP/macos-probe" + lipo "$RUNNER_TEMP/macos-probe" -verify_arch arm64 + printf 'int main(void) { unsigned __int128 v = 0, e = 0; return __atomic_compare_exchange_n(&v, &e, 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); }\n' \ + > "$RUNNER_TEMP/arm64-atomic-probe.c" + clang -target arm64-apple-macos13.3 -mcpu=apple-m1 "$RUNNER_TEMP/arm64-atomic-probe.c" \ + -o "$RUNNER_TEMP/arm64-atomic-probe" + + - name: Compile macOS POSIX host shims + run: | + mkdir -p "$RUNNER_TEMP/a64-generated" + printf '/* Generated lift helpers are unused by these host translation units. */\n' \ + > "$RUNNER_TEMP/a64-generated/a64_suyu.h" + printf '/* Generated per-game floating-point helpers are unused by these host translation units. */\n' \ + > "$RUNNER_TEMP/a64-generated/a64_fpx.h" + test "$(uname -m)" = arm64 + python -c "import platform; assert platform.machine() == 'arm64', platform.machine()" + clang -target arm64-apple-macos13.3 -mcpu=apple-m1 -fsyntax-only -D_GNU_SOURCE -Wno-everything \ + -I"$RUNNER_TEMP/a64-generated" -Ihost/posix -Ihost -Ithird_party/a64-kit/rt \ + host/posix/win32_posix.c host/hos_main.c host/rx_backend.c host/hos_setjmp.c + clang -target arm64-apple-macos13.3 -mcpu=apple-m1 -fsyntax-only -D_GNU_SOURCE -Wno-everything \ + -I"$RUNNER_TEMP/a64-generated" -Ihost/posix -Ihost -Ithird_party/a64-kit/rt \ + host/posix/display_sdl.c host/posix/macos_stubs.c + + macos-builder: + # Builds a native arm64 downloadable CLI and dependencies; it never receives or builds game data. + runs-on: macos-latest + timeout-minutes: 150 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Verify Apple Silicon runner + run: | + test "$(uname -m)" = arm64 + python -c "import platform; assert platform.machine() == 'arm64', platform.machine()" + + - uses: actions/cache@v4 + with: + path: build-macos/deps + key: macos-builder-deps-${{ hashFiles('scripts/macos/deps.sh') }} + + - name: Install dependency build tools + run: brew install cmake nasm + + - name: Build builder dependencies + run: bash scripts/macos/deps.sh + + - name: Validate bundled builder toolchain + env: + WGPU_NATIVE: ${{ github.workspace }}/build-macos/deps/wgpu-native + HOOKSHOT_SDL2: ${{ github.workspace }}/build-macos/deps/sdl2/lib/libSDL2-2.0.0.dylib + HOOKSHOT_FFMPEG: ${{ github.workspace }}/build-macos/deps/ffmpeg + run: | + build-macos/deps/python/bin/python3 -I -B tests/test_macos_build_path.py + build-macos/deps/python/bin/python3 -I -c "import tkinter" + build-macos/deps/python/bin/python3 -I -c \ + "import platform; assert platform.machine() == 'arm64', platform.machine()" + build-macos/deps/python/bin/python3 -I -c \ + "import runpy; runpy.run_path('nx2/make_game.py')['check_macos_toolchain']()" + python -I scripts/macos/check-renderer.py + lipo build-macos/deps/python/bin/python3 -verify_arch arm64 + lipo build-macos/deps/sdl2/lib/libSDL2-2.0.0.dylib -verify_arch arm64 + lipo build-macos/deps/wgpu-native/lib/libwgpu_native.dylib -verify_arch arm64 + for lib in build-macos/deps/ffmpeg/lib/*.a; do lipo "$lib" -verify_arch arm64; done + + - name: Package CLI builder + run: bash scripts/macos/package-builder.sh + + - name: Verify packaged builder starts + run: | + test -d build-macos/package/HookshotBuilder/HookshotBuilder.app + test -x build-macos/package/HookshotBuilder/HookshotBuilder.app/Contents/Resources/tools/python/bin/python3 + test -f build-macos/package/HookshotBuilder/HookshotBuilder.app/Contents/Resources/tools/source/builder/linux/hookshot_builder.py + test ! -L build-macos/package/HookshotBuilder/HookshotBuilder.app/Contents/Resources/tools + lipo build-macos/package/HookshotBuilder/HookshotBuilder.app/Contents/MacOS/applet -verify_arch arm64 + cd build-macos/package/HookshotBuilder + ./HookshotBuilder.command --help + + - uses: actions/upload-artifact@v4 + with: + name: HookshotBuilder-macos-arm64 + path: | + build-macos/HookshotBuilder-macos-arm64.zip + build-macos/HookshotBuilder-macos-arm64.app.zip + if-no-files-found: error + + release: + name: macOS release asset + if: startsWith(github.ref, 'refs/tags/v') + needs: macos-builder + runs-on: ubuntu-24.04 + permissions: + contents: write + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: HookshotBuilder-macos-arm64 + path: dist + - name: macOS builder to the release + run: scripts/release_upload.sh "$GITHUB_REF_NAME" dist/HookshotBuilder-macos-arm64.app.zip diff --git a/.gitignore b/.gitignore index 67c26d131..b17d07b13 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,10 @@ __pycache__/ # ---- Linux builds ---- build-linux/ *.so + +# ---- macOS ---- +# (the game folder a local build writes, the dependencies scripts/macos/deps.sh builds, the helpers built at a build's start) +game/ +build-macos/ +*.dylib +.DS_Store diff --git a/builder/linux/hookshot_builder.py b/builder/linux/hookshot_builder.py index 7cdb71c2a..9f969556f 100644 --- a/builder/linux/hookshot_builder.py +++ b/builder/linux/hookshot_builder.py @@ -28,7 +28,8 @@ import make_game # noqa: E402 (LANGUAGES, BUILD_ID) import toolchain # noqa: E402 (default_jobs: RX_JOBS) -CONFIG = os.path.join(os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config"), "hookshot", "builder.ini") +CONFIG = os.path.join(os.path.expanduser("~/Library/Application Support"), "Hookshot", "builder.ini") if sys.platform == "darwin" else \ + os.path.join(os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config"), "hookshot", "builder.ini") STAGES = {"read": "Reading your game files", "lift": "Translating the game to PC code", "compile": "Compiling", "finish": "Putting the game folder together"} @@ -88,6 +89,8 @@ def desktop_exec(path): def add_to_menu(folder): """an entry in the applications menu that starts the game""" + if sys.platform == "darwin": + return icon = os.path.join(folder, "hookshot.png") src = os.path.join(ROOT, "assets", "hookshot.png") if os.path.exists(src): @@ -609,7 +612,8 @@ def on_exit(self, code): except OSError: pass ttk.Button(self.buttons, text="Play", command=lambda: self.play_and_close(folder)).pack(side="right", padx=self.px(4)) - ttk.Button(self.buttons, text="Open folder", command=lambda: subprocess.Popen(["xdg-open", folder])).pack(side="right", padx=self.px(4)) + opener = "open" if sys.platform == "darwin" else "xdg-open" + ttk.Button(self.buttons, text="Open folder", command=lambda: subprocess.Popen([opener, folder])).pack(side="right", padx=self.px(4)) else: self.stage_text.set("The build stopped: %s" % getattr(self, "error", "see the lines above")) ttk.Button(self.buttons, text="Back", command=self.show_form).pack(side="right", padx=self.px(4)) diff --git a/docs/development.md b/docs/development.md index 4bf6fe61d..e111abfdc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -48,6 +48,43 @@ background when the existing "Controller in background" option or `HOS_INPUT_BAC `HOS_INPUT_BACKGROUND=0` overrides that option. `HOS_INPUT_EXCLUSIVE=1` controls merging the player's devices while the script's existing exclusive interval is active; it is not a focus override. +## macOS (Apple Silicon) + +The macOS builder and generated game are native arm64, with no Rosetta dependency. It uses SDL2 for the window and +input, and the existing WebGPU renderer through `wgpu-native`'s Metal backend; the Vulkan backend is not built on +macOS. The executable targets macOS 13.3 or newer because Apple's C++20 floating-point formatting support requires it. + +The `macOS platform` workflow uploads a `HookshotBuilder-macos-arm64` artifact containing a command-line builder, +the Hookshot source, a native arm64 Python runtime and modules, SDL2, static FFmpeg, and Metal-enabled `wgpu-native`. +It does not contain game data or a built game. Download and unpack it on the target Mac; Xcode Command Line Tools +must be installed there. Double-click `HookshotBuilder.app` to choose your own `.nup` dump and output folder, or +use the command-line launcher: + +```sh +./HookshotBuilder/HookshotBuilder.command --nup /path/to/game.nup --out /path/to/game-folder --jobs 4 +``` + +For development outside the artifact, use arm64 builds of SDL2 2.26 or newer and `wgpu-native`. `wgpu-native` must +include its matching `include/webgpu/webgpu.h` and `lib/libwgpu_native.dylib`, built with Metal support. Set +`HOOKSHOT_FFMPEG` to the static FFmpeg prefix (or provide FFmpeg through `pkg-config`) and build natively: + +```sh +export WGPU_NATIVE=/path/to/arm64-wgpu-native +export HOOKSHOT_SDL2=/path/to/arm64/libSDL2-2.0.0.dylib +export HOOKSHOT_FFMPEG=/path/to/arm64/ffmpeg +python3 -I nx2/make_game.py --nup /path/to/game.nup --out /path/to/game-folder --jobs 4 +``` + +The normal builder command validates the arm64 compiler, SDL2, FFmpeg and `wgpu-native` before lifting the game, +then places `hookshot`, `libSDL2-2.0.0.dylib` and `libwgpu_native.dylib` beside `image.bin` in the output folder; +FFmpeg is statically linked when `HOOKSHOT_FFMPEG` points to the packaged static libraries. The macOS FFmpeg build +uses its native AV1 decoder to avoid compiling an additional dav1d dependency in CI. +For lower-level development, `nx2/lift.py --standalone --write` followed by `nx2/build.py --standalone --jobs 4` +writes the executable and libraries under `work/sa/build/`. Set `RX_BACKEND=metal` to make the renderer choice +explicit (Metal is the macOS default). As on other platforms, the local game dump is required to generate and build +the game. The workflow packages and validates builder dependencies without running a game build; it is not a +notarized app release. + ## Layout | Path | What | @@ -56,7 +93,7 @@ the script's existing exclusive interval is active; it is not a focus override. | `nx2/` | the lift pipeline and its tests (mkw-recomp's, made game-neutral; `kit.py` has the paths) | | `host/` | the standalone Horizon runtime (TOTKPort's `hos/` as adapted by mkw-recomp), with the native NVN2 layer `nvn_native.c` | | `gpu/vk/` | the Vulkan back end (`hookshot_vk.dll`), with Eden's shader recompiler and sirit | -| `gpu/render/`, `gpu/third_party/` | the WebGPU renderer the exe falls back to, its op stream (`rx_nvn.h`), suyu's decoders | +| `gpu/render/`, `gpu/third_party/` | the WebGPU renderer (Metal on macOS), its op stream (`rx_nvn.h`), suyu's decoders | | `scripts/` | test runs and the profile reader | | `third_party/` | a64-kit (unmodified), libopus, zbic (Atmosphere's zstd with ZBIC, for unpacking only) | diff --git a/docs/macos-port-status.md b/docs/macos-port-status.md new file mode 100644 index 000000000..320c9cdd3 --- /dev/null +++ b/docs/macos-port-status.md @@ -0,0 +1,88 @@ +# Porte para macOS (Apple Silicon): estado em 2026-11-09 + +Branch local `macos-local`, por cima do PR #715 ("Add initial macOS build support"). O jogo roda nativo em arm64, +com o renderizador WebGPU (`gpu/render`) sobre Metal via `wgpu-native`. Testado num MacBook com Apple M5, 16 GB, +macOS 27.2, Xcode 27.2. + +## Como compilar e rodar + +```sh +bash scripts/macos/deps.sh # Python, SDL2, dav1d + FFmpeg, wgpu-native em build-macos/deps (sem Homebrew) +scripts/macos/build-game.sh --nup /caminho/do/jogo.nup --out game --language pt-BR +cd game && ./hookshot +``` + +A primeira compilação leva uns 10 minutos (lift ~1 min, 413 arquivos C em paralelo). A pasta `game/` (o jogo +montado, ~25 GB) e `build-macos/` ficam fora do repositório (`.gitignore`). Qualidade: `game/hookshot_env.txt` +(`HOS_QUALITY=alta|media|baixa|minima`); teclas fn+F8 (FSR), fn+F9 (nitidez), fn+F10 (ajustar/esticar), F11 (tela cheia). + +## O que funciona + +- Build completo nativo arm64; o jogo abre, toca áudio, reconhece controle (PS4 testado) e grava saves. +- Tela de título com o vídeo de fundo e o menu em português. +- Abertura (vídeo + legendas em português) a ~50–59 fps. +- A gameplay carrega (HUD, corações, textos), mas a cena 3D sai branca: ver "Onde paramos". + +## O que foi ajustado + +### Build e dependências +- `scripts/macos/deps.sh`: cmake, meson, ninja e pkg-config num venv quando o Mac não os tem (sem Homebrew); + dav1d compilado e FFmpeg com `libdav1d` (o decodificador AV1 nativo do FFmpeg só usa hardware: vídeos pretos). +- `nx2/zbic.py`: o sysroot do SDK passado ao clang chamado pelo caminho completo (`string.h` não encontrado). +- `nx2/build.py`: `-lc++` no link do macOS; `libwgpu_native.dylib` copiado para a pasta do jogo. +- `nx2/make_game.py`: copia `scripts/macos/hookshot_env.txt` para a pasta do jogo quando ela não tem um. +- `scripts/macos/build-game.sh`: roda o builder deste checkout com as dependências de `build-macos/deps`. + +### Runtime (`host/`) +- `posix/win32_posix.c`: + - fibras: a página de guarda colocada depois do `makecontext` (o do macOS zera a pilha inteira) e do tamanho de página real (16 KB); + - `InitializeCriticalSection` e `SleepConditionVariable*` também no macOS (faltavam no link); + - **esperas por endereço com `os_sync_wait_on_address`** (macOS 14.4+). Os "baldes" anteriores eram 64 variáveis de condição compartilhadas, e `WakeConditionVariable` acordava a thread errada (acordadas perdidas). A abertura foi de 13–16 para 50–59 fps. `HOS_WAIT_BUCKETS=1` volta ao método antigo; + - QoS `USER_INTERACTIVE` nas threads do jogo (`HOS_QOS`). +- `posix/display_sdl.c`: sem a dica `wayland,x11` no Mac (a janela não abria); teclas F7–F10 ligadas a `rx_present_hotkey`. +- `nn_video.c`: o núcleo virtual é liberado enquanto o dav1d decodifica; quadros YUV420 guardados só como NV12 (sem + converter para RGBA a cada quadro); `HOS_VIDEO_TIMES=1` mede o decodificador. +- `hos_main.c`: presets `HOS_QUALITY` (alta, media, baixa, minima) aplicados depois de ler `hookshot_env.txt`. +- `rx_backend.c`: sem o menu Insert (só existe no Vulkan), `rx_option` lê `HOS_DYNRES`, `HOS_RES_SCALE` e + `HOS_REALTIME_STEP` do ambiente, e a resolução fixa passa a funcionar no Mac. + +### Renderizador WebGPU/Metal (`gpu/render/`) +- Buffers de vértice com strides inteiros: o Metal conta `tamanho / stride` elementos e lia o último vértice como + zero (o logo do título cortado na diagonal). +- **Push constants do compute NVN2 no c0** (como `gpu/vk/vk_bank.cpp`): sem elas, as tabelas ficavam 64 bytes + adiantadas e o passo YUV→RGB dos vídeos nunca rodava (vídeos pretos, só som). +- **Base vertex, base instance e draw ID no c0** do vertex (0x640/0x644/0x648, como `GetReplaceConstBuffer` do Vulkan): + o texto da interface acha os glifos pelo base instance (todo o texto sumia). +- **Substituto das redes neurais com HMMA** (`nvn_upscale_stub`), só para programas com HMMA (`nvn_cs_matrix`): + - vídeo: a entrada RGB ampliada linearmente para a saída; + - gameplay (TAAU): a cor da cena em RGB9E5 empacotada numa imagem R32UI é decodificada no shader e elevada ao + quadrado, como `taau_upscale.comp` do Vulkan. +- **Samplers compartilhados a partir do 16º** (`RX_SHARE_SAMPLERS`, também no emissor WGSL): o Metal permite 16 por + estágio, e com 17–18 o wgpu descartava o lote inteiro de comandos do frame. +- Diagnóstico nos frame dumps: índices do desenho, fatores de blend, imagens e texturas de cada dispatch, chave do + programa. Chaves: `RX_NO_UPSCALE_STUB`, `RX_NO_CS_PUSH`. + +### Tradutor de shaders +- `gpu/third_party/suyu/.../frontend/ampere/lower_to_maxwell.h` agora é a versão do eden (`gpu/vk/eden`, a do backend + Vulkan), muito mais completa nas instruções do Switch 2. A versão intermediária, com as instruções portadas à mão + (0x8a5, 0x4a4, 0x899, 0x825, TXQ), ficou só localmente em `build-macos/lower_to_maxwell.suyu-ported.h`. +- Emissor WGSL: atômicos de imagem emulados como leitura, operação e escrita (o passe de exposição usa). + +## Onde paramos + +1. **Gameplay 3D branca.** A cena HDR é desenhada (luminância média ~22), mas a imagem final satura em 1,0. + Com `RX_SYNC_DISPATCH_WRITES=1` (copiar de volta para a memória do jogo o que os shaders de compute escrevem, a + cada frame) aparecem formas e degradê, a ~4 fps. O Vulkan teve a mesma "parede branca" e só saiu dela com correções + específicas do Zelda que o renderizador WebGPU ainda não tem: + - shaders que leem dados por ponteiros de 64 bits: ainda falham `3720e3ab9f56fbfa`, `ba00b8c5a7b43504`, + `c7f6c62cc98485ac` e `e7d75810de840595` ("WGSL global memory (64-bit addresses) is not implemented"; no Vulkan + é a tabela de ponteiros em 0x880 de `vk_bank.cpp`); + - grades de dispatch indireto da folhagem (`08b413e`); + - a sincronização dos resultados da GPU que a CPU lê (exposição automática), sem custo por frame. +2. As redes com HMMA (vídeo, TAAU e outras) não são traduzidas; os substitutos dão uma imagem um pouco mais suave. +3. Escritas de CPU em texturas `Depth32Float` são proibidas no WebGPU: cerca de 2 lotes descartados por execução. +4. Uma textura "não filtrável" pode receber um sampler compartilhado filtrável (1–2 pipelines inválidas). +5. Cosmético: caminhos no estilo Windows no log de saves e no nome do cache (`rxcache\pipelines.bin`). +6. Fps da gameplay: 20–29 com a cena ainda errada; não dá para avaliar antes de corrigir a imagem. + +Alternativa: o NX2Apple (baseado no nxnative) lista o Ocarina of Time de Switch 2 rodando no Mac via NVN sobre Metal. diff --git a/gpu/render/rx_buffer.cpp b/gpu/render/rx_buffer.cpp index 4bcf1e50b..f155f89e5 100644 --- a/gpu/render/rx_buffer.cpp +++ b/gpu/render/rx_buffer.cpp @@ -7,11 +7,11 @@ // below it). A segment is created when a draw first needs it; its pages are uploaded when a draw needs them (one // valid bit per 4 KB page; pages a dispatch wrote are copied from the neighbour that holds them). // -// Changes: the guest arena is allocated with MEM_WRITE_WATCH (hos_main.c). At the start of each GPU submission the -// pages written since the last one are invalidated (GetWriteWatch), which catches every CPU write the submission's -// data needs. Writes the command processor makes while it runs a submission (inline uploads, copies: hos/gpu.c) -// are reported through rx_memory_written; if a page they change was read earlier in the open batch, the batch is -// submitted first, so earlier draws see the old contents. +// Changes: A64_WRITE_TRACK marks guest pages written by the CPU; at the start of each GPU submission those pages are +// invalidated. Windows also uses MEM_WRITE_WATCH for writes that bypass the translated stores. Writes the command +// processor makes while it runs a submission (inline uploads, copies: hos/gpu.c) are reported through +// rx_memory_written; if a page they change was read earlier in the open batch, the batch is submitted first, so +// earlier draws see the old contents. #include #include #include @@ -362,8 +362,8 @@ BufferRef guest_buffer(uint64_t gpu_va, uint64_t size) { return {s->buffer, guest - s->base, size}; } -/// The guest pages the CPU wrote since the last call (GetWriteWatch, which also resets them): any thread. False when -/// write watching does not work (then nothing is tracked). +/// The guest pages the CPU wrote since the last call (the active write watch is reset): any thread. False when write +/// watching does not work (then nothing is tracked). bool collect_written_pages(std::vector& out) { out.clear(); #if defined(A64_WRITE_TRACK) && !defined(__EMSCRIPTEN__) @@ -392,6 +392,7 @@ bool collect_written_pages(std::vector& out) { return true; } #endif +#if !defined(__APPLE__) || !defined(A64_WRITE_TRACK) || defined(__EMSCRIPTEN__) if (!watch_ok) return false; // only the memory mapped into the GPU's address space (what the renderer can read); the cost of GetWriteWatch // grows with the range, not with the pages written @@ -448,6 +449,7 @@ bool collect_written_pages(std::vector& out) { hos_log("rx: change tracking watches %d ranges, %llu MB", nranges, (unsigned long long)(total >> 20)); } return true; +#endif } /// Pages the CPU wrote (collect_written_pages) are invalidated (called with no batch open). @@ -687,10 +689,13 @@ void buffer_cache_sync_to_guest(uint64_t gpu_va, uint64_t size) { uint8_t* dst = hos_guest_base() + p0 * kPage; // (pages the CPU wrote since the last scan keep their write-watch mark; the others are not reported as CPU // writes: the segment already holds this) + // macOS keeps these marks for collect_written_pages; only the native write-watch path resets marks here. +#if !defined(__APPLE__) || !defined(A64_WRITE_TRACK) || defined(__EMSCRIPTEN__) std::vector dirty(p1 - p0 + 1); ULONG_PTR ndirty = dirty.size(); DWORD gran = 0; const bool watched = !GetWriteWatch(0, dst, n, dirty.data(), &ndirty, &gran); +#endif const auto* src = static_cast(wgpuBufferGetConstMappedRange(staging, 0, n)); std::lock_guard lk(shadow_mu); for (uint64_t p = p0; p <= p1; p++) { @@ -708,7 +713,9 @@ void buffer_cache_sync_to_guest(uint64_t gpu_va, uint64_t size) { for (uint32_t w = 0; w < kPage / 4; w++) if (gw[w] != sw[w]) dw[w] = gw[w]; } +#if !defined(__APPLE__) || !defined(A64_WRITE_TRACK) || defined(__EMSCRIPTEN__) if (watched && ndirty == 0) ResetWriteWatch(dst, n); +#endif wgpuBufferUnmap(staging); for (uint64_t p = p0; p <= p1; p++) { gpu_pages[p] = 0; diff --git a/gpu/render/rx_compute.cpp b/gpu/render/rx_compute.cpp index e95732add..067d1b275 100644 --- a/gpu/render/rx_compute.cpp +++ b/gpu/render/rx_compute.cpp @@ -2,6 +2,7 @@ // Storage buffers are bound from the buffer cache, read-write: what a dispatch writes stays on the GPU, where the // draws and dispatches after it read it (the guest's copy of that memory is not updated). #include +#include #include "common/div_ceil.h" @@ -324,6 +325,208 @@ void dispatch(const ComputeLaunch& l) { } } +/// (macOS port, as the Vulkan back end's upscale_stub, gpu/vk/vk_draw.cpp) A compute program that does not translate here +/// (OoT's movie upscaler networks: HMMA matrix multiplies, whose emulation takes Metal minutes to compile) is replaced by +/// what the network's last pass makes: its input sampled linearly into its output, the picture a little softer. The +/// output is the largest 2D image the dispatch has bound; the input the largest texture it samples that is no image of +/// it and smaller than the output (an earlier pass of the network: none, and nothing is done). RX_NO_UPSCALE_STUB=1: +/// left out, as before. The first decision for each program is logged. +bool nvn_upscale_stub(const NvnState& s) { + static const bool off = getenv("RX_NO_UPSCALE_STUB") != nullptr; + // (only a network's pass: other programs that do not translate keep image bindings an earlier pass left, and the + // stand-in copied the scene over the game's final picture with them: the white 3D after the opening) + if (off || !nvn_cs_matrix(s)) return false; + struct Pick { + uint64_t handle = 0, area = 0; + nvn_tex t{}; + nvn_view v{}; + bool has_view = false; + uint32_t format() const { return has_view && v.set_format ? v.format : t.d.format; } // (as the shader sees it) + }; + Pick out, in; + uint64_t img_gpu[32]; + int nimg = 0; + for (int i = 0; i < 32; i++) { + Pick p; + if (!s.img[5][i] || !nvn_texture_of((int)(s.img[5][i] & 0xFFFFF), &p.t, &p.v, &p.has_view)) continue; + img_gpu[nimg++] = p.t.gpu; + if (p.t.d.depth > 1) continue; + p.handle = s.img[5][i], p.area = (uint64_t)p.t.d.width * p.t.d.height; + if (p.area > out.area) out = p; + } + // (as gpu/vk/vk_draw.cpp taau_stand_in) OoT's gameplay TAAU network: its output R11G11B10F, its first texture the scene + // colour in RGB9E5 that its first passes made, as the square root (the network's perceptual encoding, which its output + // undoes). That texture upscaled and squared; the scene's HDR colour beside it (same format as the output) upscaled as + // it was came out all white + bool squared = false; + if (out.area && out.format() == NVN_FORMAT_R11G11B10F && s.tex[5][0]) { + Pick p; + if (nvn_texture_of((int)(s.tex[5][0] & 0xFFFFF), &p.t, &p.v, &p.has_view) && p.format() == NVN_FORMAT_RGB9E5F && + p.t.d.depth <= 1 && p.t.d.width >= 64 && p.t.d.height >= 64) { + p.handle = s.tex[5][0], p.area = (uint64_t)p.t.d.width * p.t.d.height; + in = p, squared = true; + } + } + // (the network's own pass wrote that colour as packed RGB9E5 bits through an R32UI image at the same memory: WebGPU + // cannot view one as the other, and the texture came out as the gray placeholder. That image read as words, decoded) + Image* packed = nullptr; + if (squared) + for (int i = 0; i < 32 && !packed; i++) { + Pick p; + if (!s.img[5][i] || !nvn_texture_of((int)(s.img[5][i] & 0xFFFFF), &p.t, &p.v, &p.has_view)) continue; + if (p.t.gpu != in.t.gpu || p.t.d.depth > 1) continue; + Image* im = nvn_target(&p.t, p.has_view ? &p.v : nullptr); + if (im && im->format == WGPUTextureFormat_R32Uint && im->rendered) packed = im; + } + if (out.area && !squared) + for (int i = 0; i < 128; i++) { + Pick p; + if (!s.tex[5][i] || !nvn_texture_of((int)(s.tex[5][i] & 0xFFFFF), &p.t, &p.v, &p.has_view)) continue; + if (p.t.d.depth > 1 || p.t.d.target != out.t.d.target) continue; + // (the network's colour input, as its output: OoT's earlier passes sample the movie's chroma plane, RG8, + // which upscaled into the output gave junk until the last pass) + // (and a picture: the network's 4x4 placeholders are not one) + if (p.format() != out.format() || p.t.d.width < 64 || p.t.d.height < 64) continue; + if (std::find(img_gpu, img_gpu + nimg, p.t.gpu) != img_gpu + nimg) continue; // (the network's own buffers) + p.handle = s.tex[5][i], p.area = (uint64_t)p.t.d.width * p.t.d.height; + if (p.area > in.area) in = p; + } + Image* dst = out.area ? nvn_target(&out.t, out.has_view ? &out.v : nullptr) : nullptr; + const char* store = dst ? wgsl_storage_format(dst->format) : nullptr; + BoundTexture src; + if (dst && store && in.area && in.area < out.area && !packed) { + Shader::TextureDescriptor d{}; + d.type = Shader::TextureType::Color2D; + d.count = 1; + src = nvn_texture(s, in.handle, 0, d); + } + const bool ok = dst && store && in.area && in.area < out.area && + (packed ? true : src.view && src.kind == TexKind::Float); + { + static std::unordered_set logged; + if (logged.size() < 64 && logged.insert(s.prog.serial).second) + hos_log("rx: compute program (serial %llu) does not run here (grid %u,%u,%u): %s %ux%u (NVN format %#x) -> %ux%u " + "(NVN format %#x, GPU %llx, guest %llx, image GPU %llx guest %llx)", (unsigned long long)s.prog.serial, + s.grid[0], s.grid[1], s.grid[2], + ok ? (squared ? "its scene colour (squared) upscaled into its output instead, as gameplay's TAAU network:" + : "its input upscaled linearly into its output instead,") : "left out, no upscale for it:", + in.t.d.width, in.t.d.height, in.format(), out.t.d.width, out.t.d.height, out.format(), + (unsigned long long)out.t.gpu, (unsigned long long)nvn_gpu_guest(out.t.gpu), + (unsigned long long)(dst ? dst->gpu_va : 0), (unsigned long long)(dst ? dst->guest : 0)); + } + if (!ok) return false; + struct Stub { + WGPUComputePipeline pipeline = nullptr; + WGPUBindGroupLayout layout = nullptr; + }; + static std::unordered_map stubs; // (per output format, and squared or not) + static WGPUSampler linear = [] { + WGPUSamplerDescriptor sd{}; + sd.addressModeU = sd.addressModeV = sd.addressModeW = WGPUAddressMode_ClampToEdge; + sd.magFilter = sd.minFilter = WGPUFilterMode_Linear; + sd.mipmapFilter = WGPUMipmapFilterMode_Nearest; + sd.lodMaxClamp = 0.25f; + sd.maxAnisotropy = 1; + return wgpuDeviceCreateSampler(dev.device, &sd); + }(); + const uint32_t mode = packed ? 2u : squared ? 1u : 0u; // (0 plain, 1 squared, 2 packed RGB9E5 words, squared) + Stub& st = stubs[(uint32_t)dst->format << 2 | mode]; + if (!st.pipeline) { + std::string wgsl = packed ? "@group(0) @binding(0) var src: texture_2d;\n" : "@group(0) @binding(0) var src: texture_2d;\n"; + wgsl += "@group(0) @binding(1) var samp: sampler;\n" + "@group(0) @binding(2) var dst: texture_storage_2d<"; + wgsl += store; + wgsl += ", write>;\n" + "@compute @workgroup_size(8, 8) fn main(@builtin(global_invocation_id) g: vec3) {\n" + " let size = textureDimensions(dst);\n" + " if (g.x >= size.x || g.y >= size.y) { return; }\n" + " let uv = (vec2(g.xy) + 0.5) / vec2(size);\n" + ""; + if (packed) // (RGB9E5: three 9-bit mantissas and a shared exponent, bias 15; bilinear by hand, as words do not filter) + wgsl += " let n = vec2(textureDimensions(src));\n" + " let q = uv * vec2(n) - 0.5;\n" + " let i0 = vec2(floor(q));\n" + " let f = q - floor(q);\n" + " let a = e5_(src, clamp(i0, vec2(0), n - 1));\n" + " let b = e5_(src, clamp(i0 + vec2(1, 0), vec2(0), n - 1));\n" + " let c0 = e5_(src, clamp(i0 + vec2(0, 1), vec2(0), n - 1));\n" + " let d = e5_(src, clamp(i0 + vec2(1, 1), vec2(0), n - 1));\n" + " let c = mix(mix(a, b, f.x), mix(c0, d, f.x), f.y);\n" + " textureStore(dst, vec2(g.xy), vec4(c * c, 1.0));\n}\n" + "fn e5_(t: texture_2d, p: vec2) -> vec3 {\n" + " let v = textureLoad(t, p, 0).x;\n" + " let m = vec3(f32(v & 511u), f32((v >> 9u) & 511u), f32((v >> 18u) & 511u));\n" + " return m * exp2(f32(v >> 27u) - 24.0);\n}\n"; + else + wgsl += " let c = textureSampleLevel(src, samp, uv, 0.0);\n" + + std::string(squared ? " textureStore(dst, vec2(g.xy), vec4(c.rgb * c.rgb, 1.0));\n}\n" + : " textureStore(dst, vec2(g.xy), c);\n}\n"); + WGPUShaderSourceWGSL code{}; + code.chain.sType = WGPUSType_ShaderSourceWGSL; + code.code = sv(wgsl.c_str()); + WGPUShaderModuleDescriptor md{}; + md.nextInChain = &code.chain; + WGPUBindGroupLayoutEntry e[3]{}; + e[0].binding = 0, e[0].visibility = WGPUShaderStage_Compute; + e[0].texture.sampleType = packed ? WGPUTextureSampleType_Uint : WGPUTextureSampleType_Float; + e[0].texture.viewDimension = WGPUTextureViewDimension_2D; + e[1].binding = 1, e[1].visibility = WGPUShaderStage_Compute; + e[1].sampler.type = WGPUSamplerBindingType_Filtering; + e[2].binding = 2, e[2].visibility = WGPUShaderStage_Compute; + e[2].storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + e[2].storageTexture.format = dst->format; + e[2].storageTexture.viewDimension = WGPUTextureViewDimension_2D; + WGPUBindGroupLayoutDescriptor ld{}; + ld.entryCount = 3, ld.entries = e; + scope_push(); + WGPUShaderModule mod = wgpuDeviceCreateShaderModule(dev.device, &md); + st.layout = wgpuDeviceCreateBindGroupLayout(dev.device, &ld); + WGPUPipelineLayoutDescriptor pld{}; + pld.bindGroupLayoutCount = 1, pld.bindGroupLayouts = &st.layout; + WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(dev.device, &pld); + WGPUComputePipelineDescriptor cd{}; + cd.layout = pl; + cd.compute.module = mod; + cd.compute.entryPoint = sv("main"); + st.pipeline = wgpuDeviceCreateComputePipeline(dev.device, &cd); + const std::string error = scope_pop(); + wgpuPipelineLayoutRelease(pl); + wgpuShaderModuleRelease(mod); + if (!error.empty()) { + log_limited("cs_upscale_stub", 4, "rx: the upscale stand-in's pipeline was not created: %s", error.c_str()); + if (st.pipeline) wgpuComputePipelineRelease(st.pipeline); + st.pipeline = nullptr; + return false; + } + } + WGPUBindGroupEntry g[3]{}; + g[0].binding = 0, g[0].textureView = packed ? packed->attachment : src.view; + g[1].binding = 1, g[1].sampler = linear; + g[2].binding = 2, g[2].textureView = dst->attachment; + WGPUBindGroupDescriptor bd{}; + bd.layout = st.layout, bd.entryCount = 3, bd.entries = g; + WGPUBindGroup group = checked_bind_group(bd, "upscale stub"); + if (!group) return false; + end_render_pass("pe_compute"); + WGPUComputePassEncoder pass = ce_begin_compute_pass(encoder()); + cp_set_pipeline(pass, st.pipeline); + cp_set_bind_group(pass, 0, group, 0, nullptr); + cp_dispatch(pass, (dst->width + 7) / 8, (dst->height + 7) / 8, 1); + cp_end(pass); + wgpuBindGroupRelease(group); + dst->rendered = true, dst->version++; + if (frame_dump_active()) { // (RX_DUMP_SAMPLED=1: both pictures saved with the frame) + char line[160]; + std::snprintf(line, sizeof line, "upscale stand-in: %ux%u (%c) -> %ux%u", packed ? packed->width : src.width, + packed ? packed->height : src.height, packed ? 'p' : src.source, dst->width, dst->height); + dump_draw(line); + dump_sampled(dst); + if (packed) dump_sampled(packed); + else if (src.image) dump_sampled(src.image); + } + return true; +} + } // namespace // ---- the native NVN layer's dispatches: the bound program's compute stage, its banks from NVN state (c0: the @@ -342,7 +545,25 @@ void nvn_dispatch(NvnState& s, uint32_t x, uint32_t y, uint32_t z, uint64_t indi s.grid[0] = x, s.grid[1] = y, s.grid[2] = z; if (!x || !y || !z) return count("cs_empty"); ShaderModule* m = nvn_compute_module(s); - if (!m) return count("cs_skipped"); + if (frame_dump_active()) { // (macOS port) the dispatch's bound images and textures: which pass of a network reads what + char line[1600]; + int n = std::snprintf(line, sizeof line, "nvn dispatch program serial %llu key %016llx %s grid %u,%u,%u | img:", + (unsigned long long)s.prog.serial, (unsigned long long)nvn_cs_key(s), + m ? "translated" : "NOT translated", x, y, z); + const auto add = [&](const char* kind, int i, uint64_t h) { + nvn_tex t{}; + nvn_view v{}; + bool hv = false; + if (!h || n > 1500 || !nvn_texture_of((int)(h & 0xFFFFF), &t, &v, &hv)) return; + n += std::snprintf(line + n, sizeof line - n, " %s%d=%llx:%ux%ux%u/f%#x", kind, i, (unsigned long long)t.gpu, + t.d.width, t.d.height, t.d.depth, hv && v.set_format ? v.format : t.d.format); + }; + for (int i = 0; i < 32; i++) add("i", i, s.img[5][i]); + n += std::snprintf(line + n, sizeof line - n, " | tex:"); + for (int i = 0; i < 128; i++) add("t", i, s.tex[5][i]); + dump_draw(line); + } + if (!m) return count(nvn_upscale_stub(s) ? "cs_upscale_stub" : "cs_skipped"); const Shader::Info& info = m->info; if (!info.image_buffer_descriptors.empty() || !info.texture_buffer_descriptors.empty()) { log_limited("nvn_cs_images", 8, "rx: an NVN compute program uses image or texture buffers: not dispatched yet"); @@ -487,7 +708,10 @@ void nvn_dispatch(NvnState& s, uint32_t x, uint32_t y, uint32_t z, uint64_t indi buffer_entry(storage[j].first, WGPUBufferBindingType_Storage, storage[j].second.buffer, storage[j].second.offset, end[j] - storage[j].second.offset); uint32_t t_index = 0; + uint32_t last_own[2] = {0, 0}, own_samplers = 0; // (at most 16: wgsl_emit_context.cpp SetupTextures) // (RX_SHARE_SAMPLERS: from the 17th texture on, the sampler of an earlier one, as the WGSL) for (const auto& d : info.texture_descriptors) { + const bool shared_sampler = RX_SHARE_SAMPLERS && own_samplers >= 15 && last_own[d.is_depth ? 1 : 0]; + if (!shared_sampler) last_own[d.is_depth ? 1 : 0] = b + 1, own_samplers++; uint32_t primary = 0, secondary = 0; banks.read(s, 5, d.cbuf_index, d.cbuf_offset, &primary, 4); if (d.has_secondary) banks.read(s, 5, d.secondary_cbuf_index, d.secondary_cbuf_offset, &secondary, 4); @@ -512,7 +736,7 @@ void nvn_dispatch(NvnState& s, uint32_t x, uint32_t y, uint32_t z, uint64_t indi sl.sampler.type = t.kind == TexKind::Depth ? WGPUSamplerBindingType_Comparison : t.kind == TexKind::UnfilterableFloat ? WGPUSamplerBindingType_NonFiltering : WGPUSamplerBindingType_Filtering; - layout.push_back(sl); + if (!shared_sampler) layout.push_back(sl); WGPUBindGroupEntry te{}; te.binding = b; te.textureView = t.view; @@ -520,7 +744,7 @@ void nvn_dispatch(NvnState& s, uint32_t x, uint32_t y, uint32_t z, uint64_t indi WGPUBindGroupEntry se{}; se.binding = b + 1; se.sampler = t.sampler; - entries.push_back(se); + if (!shared_sampler) entries.push_back(se); key.push_back((uint32_t)t.kind); b += 2; } diff --git a/gpu/render/rx_core.cpp b/gpu/render/rx_core.cpp index dde25884e..b46352398 100644 --- a/gpu/render/rx_core.cpp +++ b/gpu/render/rx_core.cpp @@ -1,6 +1,10 @@ // The renderer's core: the WebGPU device and the window's surface, batches (one command encoder and its upload // memory per GPU submission), render targets, clears, the 3D engine's draw methods (suyu's DrawManager), present. +#if defined(_MSC_VER) #include +#elif defined(__x86_64__) +#include +#endif #include #include #include @@ -53,6 +57,9 @@ bool wgpuDevicePoll(WGPUDevice device, bool wait, const void* submission) { #endif namespace rx { +#ifdef __APPLE__ +extern "C" void hos_posix_window_pixel_size(int* width, int* height); +#endif bool draw_must_run = false; bool rt_history_on() { static const bool on = getenv("RX_RT_HISTORY") != nullptr; return on; } @@ -340,12 +347,17 @@ bool init_device(HWND hwnd) { #else WGPUInstanceExtras extras{}; extras.chain.sType = (WGPUSType)WGPUSType_InstanceExtras; +#ifdef __APPLE__ + extras.backends = WGPUInstanceBackend_Metal; +#else const char* be = getenv("RX_BACKEND"); extras.backends = !be ? WGPUInstanceBackend_Primary : !_stricmp(be, "vulkan") ? WGPUInstanceBackend_Vulkan : !_stricmp(be, "dx12") ? WGPUInstanceBackend_DX12 : !_stricmp(be, "gl") ? WGPUInstanceBackend_GL + : !_stricmp(be, "metal") ? WGPUInstanceBackend_Metal : WGPUInstanceBackend_Primary; +#endif if (getenv("RX_VALIDATION")) extras.flags = WGPUInstanceFlag_Validation | WGPUInstanceFlag_Debug; // RX_SHADER_NAMES=1: the debug flag alone (no validation layers): naga names SPIR-V variables after the WGSL ones, // so RenderDoc's shader debugger shows f_0, u_12, in_attr3... of the dumped WGSL (RX_DUMP_WGSL) @@ -383,6 +395,14 @@ bool init_device(HWND hwnd) { return false; } if (hwnd) { +#ifdef __APPLE__ + WGPUSurfaceSourceMetalLayer src{}; + src.chain.sType = WGPUSType_SurfaceSourceMetalLayer; + src.layer = hwnd; + WGPUSurfaceDescriptor sd{}; + sd.nextInChain = &src.chain; + surface = wgpuInstanceCreateSurface(dev.instance, &sd); +#else WGPUSurfaceSourceWindowsHWND src{}; src.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; src.hinstance = GetModuleHandleA(nullptr); @@ -390,6 +410,7 @@ bool init_device(HWND hwnd) { WGPUSurfaceDescriptor sd{}; sd.nextInChain = &src.chain; surface = wgpuInstanceCreateSurface(dev.instance, &sd); +#endif } #endif @@ -560,6 +581,11 @@ bool init_device(HWND hwnd) { #ifdef __EMSCRIPTEN__ present_mode = WGPUPresentMode_Fifo; configure_surface(1280, 720); +#elif defined(__APPLE__) + int width = 0, height = 0; + hos_posix_window_pixel_size(&width, &height); + configure_surface((uint32_t)std::max(width, 1), (uint32_t)std::max(height, 1)); + present_window_size(width, height); #else RECT rc; GetClientRect(hwnd, &rc); @@ -1344,6 +1370,7 @@ bool stats_on = false; int64_t clock_ns() { if (!stats_on) return 0; +#if defined(__x86_64__) || defined(_M_X64) static const double ns_per_tick = [] { const int64_t t0 = hos_now_ns(); const uint64_t c0 = __rdtsc(); @@ -1353,6 +1380,9 @@ int64_t clock_ns() { return c1 > c0 ? double(t1 - t0) / double(c1 - c0) : 0.0; }(); return (int64_t)(double(__rdtsc()) * ns_per_tick); +#else + return hos_now_ns(); +#endif } bool gpu_read(uint64_t va, void* dst, uint64_t n) { diff --git a/gpu/render/rx_draw.cpp b/gpu/render/rx_draw.cpp index 3e3729f56..fe0d25b4d 100644 --- a/gpu/render/rx_draw.cpp +++ b/gpu/render/rx_draw.cpp @@ -797,10 +797,10 @@ void stage_resources(StageResources& out, const Channel& ch, const Regs& r, cons if (frame_dump_active()) dump_textures.clear(); // (web: from the 17th texture on, a texture shares the sampler of the last earlier one of its kind that has its // own, as the WGSL backend declares them: browsers allow 16 samplers a stage) - uint32_t last_own[2] = {0, 0}; + uint32_t last_own[2] = {0, 0}, own_samplers = 0; // (at most 16: wgsl_emit_context.cpp SetupTextures) for (const auto& d : info.texture_descriptors) { - const bool shared_sampler = RX_WEB && t_index >= 16 && last_own[d.is_depth ? 1 : 0]; - if (!shared_sampler) last_own[d.is_depth ? 1 : 0] = b + 1; + const bool shared_sampler = RX_SHARE_SAMPLERS && own_samplers >= 15 && last_own[d.is_depth ? 1 : 0]; + if (!shared_sampler) last_own[d.is_depth ? 1 : 0] = b + 1, own_samplers++; const uint32_t handle = read_handle(ch, stage, d); if (static const bool trace_bindings = env_flag("RX_TRACE_BINDINGS"); trace_bindings) { static std::unordered_map seen; @@ -2789,13 +2789,15 @@ bool nvn_vertex_input(VertexInput& vi, const NvnState& s, const Shader::Info& in uint32_t end = 0; for (const Attr& a : streams[st]) end = std::max(end, a.offset + a.size); if (cached.on) { + // (whole strides, as below: wgpu's Metal back end reads the last element of a shorter binding as zeros) + const auto whole = [&](uint64_t n) { return stride ? (n + stride - 1) / stride * stride : n; }; uint64_t bytes; - if (cached.indirect) bytes = std::max(vs.size, end); + if (cached.indirect) bytes = whole(std::max(vs.size, end)); else if (!stride) bytes = end; - else if (instanced) bytes = (uint64_t)(base_instance + instances - 1) * stride + end; - else if (!cached.indexed) bytes = (uint64_t)(cached.first + cached.count - 1) * stride + end; - else bytes = std::max(vs.size, end); - bytes = std::min((bytes + 3) & ~3ull, std::max((vs.size + 3) & ~3ull, (end + 3) & ~3ull)); + else if (instanced) bytes = (uint64_t)(base_instance + instances) * stride; + else if (!cached.indexed) bytes = (uint64_t)(cached.first + cached.count) * stride; + else bytes = whole(std::max(vs.size, end)); + bytes = std::min((bytes + 3) & ~3ull, std::max((whole(vs.size) + 3) & ~3ull, (end + 3) & ~3ull)); if (vs.addr & 3) { count("nc_align"); return false; @@ -2824,7 +2826,10 @@ bool nvn_vertex_input(VertexInput& vi, const NvnState& s, const Shader::Info& in } const uint64_t first = instanced ? base_instance : v0; const uint64_t n_el = instanced ? instances : (uint64_t)v1 - v0 + 1; - uint64_t bytes = stride ? (n_el - 1) * stride + end : end; + // (whole strides, the last element's too: wgpu's Metal back end counts a binding's elements as size / stride, so + // a binding of (n - 1) * stride + end bytes, valid in WebGPU, read its last vertex as zeros. The title's + // composite quad (4 vertices of 16 bytes, 8 used: 56 bytes) then lost a triangle, and the logo half the screen) + uint64_t bytes = stride ? n_el * stride : end; bytes = (bytes + 3) & ~3ull; if (bytes > (64ull << 20)) return false; const uint64_t off = stream_ro.alloc(bytes, 16); @@ -3146,10 +3151,10 @@ void nvn_stage_resources(StageResources& out, const NvnState& s, int stage, cons uint32_t t_index = 0; // (web: from the 17th texture on, a texture shares the sampler of the last earlier one of its kind that has its // own, as the WGSL backend declares them and stage_resources binds them: browsers allow 16 samplers a stage) - uint32_t last_own[2] = {0, 0}; + uint32_t last_own[2] = {0, 0}, own_samplers = 0; // (at most 16: wgsl_emit_context.cpp SetupTextures) for (const auto& d : info.texture_descriptors) { - const bool shared_sampler = RX_WEB && t_index >= 16 && last_own[d.is_depth ? 1 : 0]; - if (!shared_sampler) last_own[d.is_depth ? 1 : 0] = b + 1; + const bool shared_sampler = RX_SHARE_SAMPLERS && own_samplers >= 15 && last_own[d.is_depth ? 1 : 0]; + if (!shared_sampler) last_own[d.is_depth ? 1 : 0] = b + 1, own_samplers++; uint32_t primary = 0, secondary = 0; banks.read(s, stage, d.cbuf_index, d.cbuf_offset, &primary, 4); if (d.has_secondary) banks.read(s, stage, d.secondary_cbuf_index, d.secondary_cbuf_offset, &secondary, 4); @@ -3473,7 +3478,7 @@ void nvn_draw(NvnState& s, const NvnDraw& d) { // the frame rate depends on the replay's time (what making it faster could give) if (static const int64_t spin = getenv("RX_DEBUG_DRAW_SPIN_NS") ? atoll(getenv("RX_DEBUG_DRAW_SPIN_NS")) : 0; spin) { const int64_t until = clock_ns() + spin; - while (clock_ns() < until) _mm_pause(); + while (clock_ns() < until) cpu_relax(); } struct DrawTimer { int64_t t0 = clock_ns(); @@ -3493,6 +3498,10 @@ void nvn_draw(NvnState& s, const NvnDraw& d) { count(why); }; if (!d.count) return count("draws_empty"); + // (macOS port, as gpu/vk) the draw macros' parameters in the vertex stage's c0 (NvnState::draw_base_vertex) + s.draw_base_vertex = d.indexed ? d.base_vertex : (int32_t)d.first; + s.draw_base_instance = d.base_instance; + s.draw_id = 0; trim_bind_groups(); const Attachments at = nvn_attachments(s); if (!at.width) return skip("no render target"); @@ -3835,6 +3844,13 @@ void nvn_draw(NvnState& s, const NvnDraw& d) { if (c->operation == WGPUBlendOperation_Min || c->operation == WGPUBlendOperation_Max) c->srcFactor = c->dstFactor = WGPUBlendFactor_One; t.blend = &blends[i]; + if (frame_dump_active()) { // (the blend as WebGPU gets it: op, src, dst for colour and alpha) + char line[160]; + std::snprintf(line, sizeof line, " blend %u: colour %d %d %d alpha %d %d %d", i, + (int)b.color.operation, (int)b.color.srcFactor, (int)b.color.dstFactor, + (int)b.alpha.operation, (int)b.alpha.srcFactor, (int)b.alpha.dstFactor); + dump_draw(line); + } key.insert(key.end(), {(uint32_t)b.color.operation, (uint32_t)b.color.srcFactor, (uint32_t)b.color.dstFactor, (uint32_t)b.alpha.operation, (uint32_t)b.alpha.srcFactor, (uint32_t)b.alpha.dstFactor}); @@ -4054,6 +4070,23 @@ void nvn_draw(NvnState& s, const NvnDraw& d) { // RX_DUMP_ATTRS=[,...]: their vertex attributes (format, stream) and the first 2048 // elements' values static const std::vector attrs_vs = dump_attrs_programs(); + if (d.indexed && std::find(attrs_vs.begin(), attrs_vs.end(), prog->vs.key) != attrs_vs.end()) { + // (the draw's indices as guest memory holds them, and the topology and index format it was drawn with) + const uint32_t n = std::min(d.count, 24); + std::vector raw((size_t)n * index_size); + gpu_read(d.index_gpu, raw.data(), raw.size()); + std::string list; + for (uint32_t i = 0; i < n; i++) { + const uint8_t* q = raw.data() + (size_t)i * index_size; + const uint32_t v = index_size == 1 ? q[0] : index_size == 2 ? (uint32_t)(q[0] | q[1] << 8) + : (uint32_t)(q[0] | q[1] << 8 | q[2] << 16 | q[3] << 24); + list += std::to_string(v) + " "; + } + std::snprintf(b, sizeof b, " indices (%u bytes each, at %llx, base vertex %d, cached %d, topology %d, format %d, front %d): %s", + index_size, (unsigned long long)d.index_gpu, (int)d.base_vertex, (int)cached, (int)topology, + (int)index_format, (int)s.poly.front, list.c_str()); + dump_draw(b); + } if (std::find(attrs_vs.begin(), attrs_vs.end(), prog->vs.key) != attrs_vs.end()) { for (uint32_t i = 0; i < 16; i++) { if (!(prog->vs.generic_loads() >> i & 1)) continue; diff --git a/gpu/render/rx_internal.h b/gpu/render/rx_internal.h index 18c7f0917..8e83a1858 100644 --- a/gpu/render/rx_internal.h +++ b/gpu/render/rx_internal.h @@ -11,6 +11,22 @@ #include #include #include +#include +#if defined(_MSC_VER) +#include +#elif defined(__x86_64__) +#include +#endif + +inline void cpu_relax() { +#if defined(__aarch64__) || defined(__arm64__) || defined(_M_ARM64) + __asm__ volatile("yield" ::: "memory"); +#elif defined(__x86_64__) || defined(_M_X64) + _mm_pause(); +#else + std::this_thread::yield(); +#endif +} #include #ifdef __EMSCRIPTEN__ @@ -24,6 +40,14 @@ bool wgpuDevicePoll(WGPUDevice device, bool wait, const void* submission); // #define RX_WEB 0 #include #endif +// (macOS port) Samplers shared past 16 a stage, as the WGSL backend declares them (wgsl_emit_context.cpp SetupTextures): +// browsers allow 16, and so does Metal. With 17 or 18 OoT's pixel programs' layouts were invalid there, and every +// batch holding one of their draws was dropped: the 3D white after the opening +#if defined(__EMSCRIPTEN__) || defined(__APPLE__) +#define RX_SHARE_SAMPLERS 1 +#else +#define RX_SHARE_SAMPLERS 0 +#endif #ifndef RX_WEB_MT #define RX_WEB_MT 0 // (the threaded web build: rx_web_mt.h) #endif @@ -602,6 +626,13 @@ struct NvnState { // per NVN stage (0 vertex, 1 fragment, 2 geometry, 3 tess control, 4 tess evaluation, 5 compute) nvn_p_bind_buf ubo[6][16]{}, ssbo[6][16]{}; uint64_t tex[6][128]{}, samp[6][128]{}, img[6][32]{}; + uint8_t cs_push[0x400]{}; // (macOS port, as gpu/vk) compute push constants (PushComputeConstants), c0 from 0x10 + uint32_t cs_push_n = 0; + // (macOS port, as gpu/vk/vk_shader.cpp GetReplaceConstBuffer) the draw being prepared: its base vertex, base instance + // and draw index, which NVN's draw macros put in the vertex stage's c0 at 0x640, 0x644 and 0x648 (gl_BaseVertex, + // gl_BaseInstance, gl_DrawID). OoT's text finds each string's glyph records with the base instance there. + int32_t draw_base_vertex = 0; + uint32_t draw_base_instance = 0, draw_id = 0; nvn_blend_st blend[8]{}; // by target nvn_color_st color{}; nvn_chmask_st chmask{0xffffffffu}; @@ -678,6 +709,7 @@ ProgramPair* nvn_programs(const NvnState& s, const uint8_t out_types[8]); // rx const std::vector* nvn_c1(const NvnState& s, int stage); /// The bound program's compute code key (its RX_DUMP_WGSL name nvn_cs_), 0 without one. uint64_t nvn_cs_key(const NvnState& s); +bool nvn_cs_matrix(const NvnState& s); // (macOS port) native matrix multiplies in the compute program ShaderModule* nvn_compute_module(const NvnState& s, const std::array* ssbo_alias = nullptr, const std::array* ssbo_alias_words = nullptr, const std::array* image_formats = nullptr); diff --git a/gpu/render/rx_nvn.cpp b/gpu/render/rx_nvn.cpp index 453881761..41e096c71 100644 --- a/gpu/render/rx_nvn.cpp +++ b/gpu/render/rx_nvn.cpp @@ -254,9 +254,13 @@ bool nvn_sampler_of(int id, nvn_samp_par* p) { // image handles (8 bytes a slot), as many slots of each as the program's control section says (+0xb1 uniform // buffers, +0xb2 storage buffers, +0xb3 textures, +0xb6 images). Found from 173 programs' reads, all of which fit. // Graphics programs keep the fixed layout below (c2 texture handles at 0x20 + 8 slot). +// (macOS port, as gpu/vk/vk_bank.cpp) with push constants first, right after the counts: +0xa9 is their size in words +// (OoT's NV12 -> RGBA shader: 15), the area rounded to 16 bytes. Without it the tables sat 64 bytes early, that shader +// read its output image's handle as 0 and was never dispatched: the movies stayed black. struct Nvn2CsLayout { - uint32_t ubo = 0, ssbo = 0, tex = 0, img = 0; // slot counts - uint32_t ssbo_at() const { return 0x10 + 16 * ubo; } + uint32_t ubo = 0, ssbo = 0, tex = 0, img = 0, push = 0; // slot counts; push: bytes + uint32_t ubo_at() const { return 0x10 + push; } + uint32_t ssbo_at() const { return ubo_at() + 16 * ubo; } uint32_t tex_at() const { return ssbo_at() + 16 * ssbo; } uint32_t img_at() const { return tex_at() + 8 * tex; } uint32_t end() const { return img_at() + 8 * img; } @@ -276,6 +280,8 @@ Nvn2CsLayout nvn2_cs_layout(const NvnState& s) { gpu_read(s.prog.sd[i].control + 0x56, &st, 2); if (magic != 0x98761234 || st != 5) continue; l.ubo = c[0xb1 - 0x58], l.ssbo = c[0xb2 - 0x58], l.tex = c[0xb3 - 0x58], l.img = c[0xb6 - 0x58]; + static const bool no_push = getenv("RX_NO_CS_PUSH") != nullptr; // (debugging: the old layout) + l.push = no_push ? 0 : ((uint32_t)c[0xa9 - 0x58] * 4 + 15) & ~15u; break; } if (by_serial.size() > 4096) by_serial.clear(); @@ -295,7 +301,8 @@ void nvn_driver_bank(const NvnState& s, int bank, int stage, uint8_t d[0xA00]) { std::memcpy(d + at, &e.addr, 8); std::memcpy(d + at + 8, &n, 4); }; - for (uint32_t r = 0; r < l.ubo && r < 16; r++) entry(0x10 + 16 * r, s.ubo[5][r]); + std::memcpy(d + 0x10, s.cs_push, std::min(std::min(l.push, s.cs_push_n), 0xA00 - 0x10)); + for (uint32_t r = 0; r < l.ubo && r < 16; r++) entry(l.ubo_at() + 16 * r, s.ubo[5][r]); for (uint32_t r = 0; r < l.ssbo && r < 16; r++) entry(l.ssbo_at() + 16 * r, s.ssbo[5][r]); for (uint32_t r = 0; r < l.tex && r < 128 && l.tex_at() + 8 * r + 4 <= 0xA00; r++) { const uint32_t th = (uint32_t)s.tex[5][r]; @@ -327,6 +334,9 @@ void nvn_driver_bank(const NvnState& s, int bank, int stage, uint8_t d[0xA00]) { } else if (stage == 0) { const float point_max = 2047.9375f; std::memcpy(d + 0x610, &point_max, 4); + std::memcpy(d + 0x640, &s.draw_base_vertex, 4); // (the draw macros' parameters, see NvnState) + std::memcpy(d + 0x644, &s.draw_base_instance, 4); + std::memcpy(d + 0x648, &s.draw_id, 4); } } else if (bank == 2) { for (int r = 0; r < 128; r++) { @@ -371,7 +381,13 @@ uint32_t nvn_driver_bank_word(const NvnState& s, int bank, int stage, uint32_t o auto entry = [&](const nvn_p_bind_buf& e, uint32_t in) -> uint32_t { return in == 0 ? lo(e.addr) : in == 4 ? hi(e.addr) : in == 8 ? (uint32_t)e.size : 0; }; - if (offset < l.ssbo_at()) return (offset - 0x10) / 16 < 16 ? entry(s.ubo[5][(offset - 0x10) / 16], (offset - 0x10) % 16) : 0; + if (offset < l.ubo_at()) { // (the push area) + uint32_t v = 0; + if (offset - 0x10 + 4 <= s.cs_push_n) std::memcpy(&v, s.cs_push + (offset - 0x10), 4); + return v; + } + if (offset < l.ssbo_at()) + return (offset - l.ubo_at()) / 16 < 16 ? entry(s.ubo[5][(offset - l.ubo_at()) / 16], (offset - l.ubo_at()) % 16) : 0; if (offset < l.tex_at()) return (offset - l.ssbo_at()) / 16 < 16 ? entry(s.ssbo[5][(offset - l.ssbo_at()) / 16], (offset - l.ssbo_at()) % 16) : 0; if (offset < l.img_at()) return (offset & 7) ? 0 : lo(s.tex[5][std::min((offset - l.tex_at()) / 8, 127)]); @@ -394,6 +410,9 @@ uint32_t nvn_driver_bank_word(const NvnState& s, int bank, int stage, uint32_t o } if (base && offset >= base && offset < base + 16 * 16) return table(s.ssbo[stage], base); if (stage == 0 && offset == 0x610) return std::bit_cast(2047.9375f); + if (stage == 0 && offset == 0x640) return (uint32_t)s.draw_base_vertex; // (the draw macros' parameters) + if (stage == 0 && offset == 0x644) return s.draw_base_instance; + if (stage == 0 && offset == 0x648) return s.draw_id; return 0; } @@ -990,6 +1009,14 @@ void cond_recheck_present() { break; } case NOP_BLEND_COLOR: std::memcpy(s.blend_color, payload(a, avail).rgba, 16); break; + case NOP_PUSH_CONST: { // (macOS port, as gpu/vk/vk_replay.cpp) compute push constants, served from c0 + const auto u = payload(a, avail); + if (u.size && u.size <= avail - sizeof u && (uint64_t)u.offset + u.size <= sizeof s.cs_push) { + std::memcpy(s.cs_push + u.offset, a + sizeof u, u.size); + s.cs_push_n = std::max(s.cs_push_n, u.offset + u.size); + } + break; + } case NOP_PRIMITIVE_RESTART: { const auto r = payload(a, avail); s.restart = r.enable != 0, s.restart_index = (uint32_t)r.index; diff --git a/gpu/render/rx_shader.cpp b/gpu/render/rx_shader.cpp index 014bcbfd2..01f2ef812 100644 --- a/gpu/render/rx_shader.cpp +++ b/gpu/render/rx_shader.cpp @@ -1008,6 +1008,7 @@ struct NvnCode { uint32_t local[3]{}; uint32_t shared = 0, local_memory = 0; // compute: workgroup shared memory, local memory bytes bool ok = false; + bool matrix = false; // (macOS port) it has native matrix multiplies (HMMA, opcode 0x23c): an upscaler network's pass }; constexpr uint32_t kNvn2ControlMagic = 0x98761234; @@ -1044,6 +1045,7 @@ NvnCode read_nvn_code(const nvn_stage_data& sd) { for (uint32_t i = 0; hdr + (i + 1) * sizeof(Instruction) <= len; i++) { Instruction& insn = native.emplace_back(); std::memcpy(&insn, area.data() + hdr + i * sizeof(Instruction), sizeof insn); + if (insn.Bits(0, 12) == 0x23c) c.matrix = true; const auto r = capture.Observe(insn, i); if (r == Shader::Ampere::ProgramCaptureState::Result::InvalidTarget) { log_limited("nvn_capture", 8, "rx: NVN2 program %016llx: branch outside the program at +%#x", @@ -1276,6 +1278,7 @@ const NvnCode* nvn_cs_code(const NvnState& s) { c = std::make_unique(); for (uint32_t i = 0; i < s.prog.nstage && i < 6; i++) { NvnCode k = read_nvn_code(s.prog.sd[i]); + if (k.stage == 5 && k.matrix) c->matrix = true; // (kept when it does not lower too: nvn_cs_matrix) if (k.ok && k.stage == 5) { *c = std::move(k); break; @@ -1292,6 +1295,14 @@ uint64_t nvn_cs_key(const NvnState& s) { return code ? code->key : 0; } +/// (macOS port) whether the bound compute program has native matrix multiplies, translated or not (nvn_upscale_stub) +bool nvn_cs_matrix(const NvnState& s) { + if (!s.has_prog) return false; + nvn_cs_code(s); // (read and kept, failed or not) + const auto it = nvn_cs_codes.find(s.prog.serial); + return it != nvn_cs_codes.end() && it->second && it->second->matrix; +} + ShaderModule* nvn_compute_module(const NvnState& s, const std::array* ssbo_alias, const std::array* ssbo_alias_words, const std::array* image_formats) { diff --git a/gpu/third_party/suyu/shader_recompiler/backend/wgsl/emit_wgsl_image.cpp b/gpu/third_party/suyu/shader_recompiler/backend/wgsl/emit_wgsl_image.cpp index 8d67f635c..bba5eb6ee 100644 --- a/gpu/third_party/suyu/shader_recompiler/backend/wgsl/emit_wgsl_image.cpp +++ b/gpu/third_party/suyu/shader_recompiler/backend/wgsl/emit_wgsl_image.cpp @@ -425,22 +425,35 @@ void EmitImageWrite(EmitContext& ctx, IR::Inst& inst, const IR::Value& index, kind == 0 ? std::string(color) : fmt::format("bitcast>({})", kind == 1 ? "f32" : "i32", color)); } -#define IMAGE_ATOMIC(name) \ - void EmitImageAtomic##name(EmitContext&, IR::Inst&, const IR::Value&, std::string_view, \ - std::string_view) { \ - throw NotImplementedException("WGSL image atomics"); \ - } -IMAGE_ATOMIC(IAdd32) -IMAGE_ATOMIC(SMin32) -IMAGE_ATOMIC(UMin32) -IMAGE_ATOMIC(SMax32) -IMAGE_ATOMIC(UMax32) -IMAGE_ATOMIC(Inc32) -IMAGE_ATOMIC(Dec32) -IMAGE_ATOMIC(And32) -IMAGE_ATOMIC(Or32) -IMAGE_ATOMIC(Xor32) -IMAGE_ATOMIC(Exchange32) +// (macOS port) Image atomics as a read, the operation and a write of the texel (WebGPU's storage textures have no atomics +// without wgpu's native TextureAtomic access, which its C API cannot name in a layout). Exact where one invocation at a +// time touches a texel; OoT's exposure pass (4dc3be520667a40f, one workgroup) used them and the 3D came out white +// without it. The texel's old value is the result, as the atomic's. +#define IMAGE_ATOMIC(name, expr) \ + void EmitImageAtomic##name(EmitContext& ctx, IR::Inst& inst, const IR::Value& index, std::string_view coords, \ + std::string_view value) { \ + (void)index; \ + const auto info{inst.Flags()}; \ + const u32 kind = ctx.images.at(info.descriptor_index).kind; \ + const std::string img{Image(ctx, info)}, c{LoadCoords(info, coords)}, v{value}; \ + const std::string load{kind == 0 ? fmt::format("textureLoad({},{}).x", img, c) \ + : fmt::format("bitcast(textureLoad({},{}).x)", img, c)}; \ + const char* type = kind == 0 ? "u32" : kind == 1 ? "f32" : "i32"; \ + const auto ret{ctx.var_alloc.Define(inst, WgslVarType::U32)}; \ + ctx.Add("{{let o_={};let v_={};let n_={};textureStore({},{},vec4<{}>(bitcast<{}>(n_)));{}=o_;}}", load, v, \ + expr, img, c, type, type, ret); \ + } +IMAGE_ATOMIC(IAdd32, "o_+v_") +IMAGE_ATOMIC(SMin32, "bitcast(min(bitcast(o_),bitcast(v_)))") +IMAGE_ATOMIC(UMin32, "min(o_,v_)") +IMAGE_ATOMIC(SMax32, "bitcast(max(bitcast(o_),bitcast(v_)))") +IMAGE_ATOMIC(UMax32, "max(o_,v_)") +IMAGE_ATOMIC(Inc32, "select(o_+1u,0u,o_>=v_)") +IMAGE_ATOMIC(Dec32, "select(o_-1u,v_,o_==0u||o_>v_)") +IMAGE_ATOMIC(And32, "o_&v_") +IMAGE_ATOMIC(Or32, "o_|v_") +IMAGE_ATOMIC(Xor32, "o_^v_") +IMAGE_ATOMIC(Exchange32, "v_") #undef IMAGE_ATOMIC void EmitIsTextureScaled(EmitContext& ctx, IR::Inst& inst, const IR::Value& index) { diff --git a/gpu/third_party/suyu/shader_recompiler/backend/wgsl/wgsl_emit_context.cpp b/gpu/third_party/suyu/shader_recompiler/backend/wgsl/wgsl_emit_context.cpp index b1e389870..563d9e732 100644 --- a/gpu/third_party/suyu/shader_recompiler/backend/wgsl/wgsl_emit_context.cpp +++ b/gpu/third_party/suyu/shader_recompiler/backend/wgsl/wgsl_emit_context.cpp @@ -298,17 +298,18 @@ void EmitContext::SetupTextures(Bindings& bindings) { // Browsers allow 16 samplers a stage (wgpu more): from the 17th texture on, a texture samples with the sampler of // the last earlier texture of its kind that has one (gpu/render/rx_draw.cpp binds the same way) u32 last_own[2] = {0, 0}; // the last texture with a sampler of its own, by is_depth (binding + 1; 0: none) + u32 own = 0; // (macOS port) samplers declared: 15, and a 16th only for the first texture of the other kind for (const auto& desc : info.texture_descriptors) { if (desc.count > 1) { throw NotImplementedException("WGSL texture arrays"); } const u32 binding{bindings.unified}; u32 shared{0}; -#ifdef __EMSCRIPTEN__ - if (textures.size() >= 16) shared = last_own[desc.is_depth ? 1 : 0]; // (binding + 1) +#if defined(__EMSCRIPTEN__) || defined(__APPLE__) // (macOS port: Metal allows 16 too; gpu/render RX_SHARE_SAMPLERS) + if (own >= 15) shared = last_own[desc.is_depth ? 1 : 0]; // (binding + 1; 0: none of its kind yet, its own) #endif textures.push_back({binding, desc.count, shared}); - if (!shared) last_own[desc.is_depth ? 1 : 0] = binding + 1; + if (!shared) last_own[desc.is_depth ? 1 : 0] = binding + 1, own++; const auto type{desc.is_depth ? DepthTextureType(desc.type) : ColorTextureType(desc.type, desc.is_multisample)}; header += fmt::format("@group({}) @binding({}) var tex{}: {};\n", group, binding, binding, type); diff --git a/gpu/third_party/suyu/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/gpu/third_party/suyu/shader_recompiler/frontend/ampere/lower_to_maxwell.h index d13cd11ff..13e80bcf4 100644 --- a/gpu/third_party/suyu/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/gpu/third_party/suyu/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -29,11 +29,97 @@ struct Instruction { }; static_assert(sizeof(Instruction) == 16); +// (hookshot) set by the Vulkan back end when the GPU's cooperative matrices passed its self-test +// (vk_coopmat.cpp): HMMA.16816.F16 then lowers to the HMMA_nx2 pseudo instruction (one subgroup +// matrix multiply on the host's tensor cores) instead of the shuffle expansion. +inline thread_local bool native_matrix_multiply = false; + +// (hookshot) Native programs whose GPRs reach the lowering's reserved registers (the uniform bank at R216-R245, R239's +// uniform predicates, the scratch R246-R254): OoT's big vertex and mesh programs use ~249 GPRs. Their high GPRs (from +// the reserved base up) live in IR registers 256 + n instead: each lowered word of a native instruction that names +// them is preceded by REGMAP_nx2 words (Maxwell 0xefe0, frontend/maxwell/translate/impl/register_map_nx2.cpp), and +// the translator reads those numbers in the next word as the extended registers. A high GPR in the scratch range +// (R246 up) or R239, which the lowering's own words name too, is renamed first: its field gets a marker, a number of +// the uniform bank the program never uses, and the prefix maps the marker to the register. Two runs a prefix: numbers +// at bits 0-7 and 20-27, targets at 8-15 and 28-35 (16-19 is the predicate, PT), lengths - 1 at 36-39, the run count +// at 40-41. +struct RegisterRun { u32 number, target, length; }; +inline u64 RegisterMapPrefix(const RegisterRun* runs, u32 count) { + u64 w = 0xefe0000000000000ULL | (u64{7} << 16) | (u64{count} << 40); + constexpr u32 number_at[2]{0, 20}, target_at[2]{8, 28}; + for (u32 i = 0; i < count; ++i) { + w |= u64{runs[i].number & 0xff} << number_at[i]; + w |= u64{runs[i].target & 0xff} << target_at[i]; + w |= u64{(runs[i].length - 1) & 3} << (36 + 2 * i); + } + return w; +} +// (hookshot) Set while a program that failed with the rules above is lowered again (LowerToMaxwell): more fields +// renamed and their exact widths. Only for those, so that every program that lowered before lowers to the same words. +inline thread_local bool extended_renaming = false; +// Fields that hold GPRs in the opcodes whose high GPRs may be renamed (ALU forms as operand() reads them, and the +// global loads and stores): 1 dest, 2 Ra, 4 Rb at 32, 8 at 64 (Rc, or Rb in forms 2, 3 and 7) +inline u32 RenameableFields(u32 opcode) { + const u32 family = opcode & 0x1ff, form = (opcode >> 9) & 7; + switch (opcode) { + case 0x381: case 0x981: return 1 | 2; // LDG Rd, [Ra] (0x381: Ra a register pair) + case 0x986: return 2 | 4; // STG [Ra], Rb + default: break; + } + if (extended_renaming) { + switch (opcode) { + case 0x805: return 1; // (hookshot) CS2R.64 Rd, SRZ: the pair it zeroes + case 0x308: return 1 | 4; // (hookshot) MUFU Rd, Rb + case 0x387: case 0x987: return 2 | 4; // (hookshot) STL [Ra..], Rb + case 0x983: return 1 | 2; // (hookshot) LDL Rd, [Ra..] + default: break; + } + // (hookshot) ISETP and FSETP: Ra, and Rb in the register form (their destinations are predicates) + if (family == 0x00c || family == 0x00b) return form >= 1 && form <= 6 ? 2 | (form == 1 ? 4 : 0) : 0; + } + const bool three = family == 0x010 || family == 0x011 || family == 0x012 || family == 0x016 || family == 0x019 || + family == 0x023 || family == 0x024 || family == 0x025 || family == 0x027; + const bool two = family == 0x020 || family == 0x021 || family == 0x008 || family == 0x007 || family == 0x009 || + family == 0x017; + // (hookshot: extended, form 6 too, a uniform register at 32 that is no GPR field: Rd, Ra and Rc are; OoT's mesh + // programs computed into R226 while UR10 lived there) + if ((!three && !two) || form < 1 || form > (extended_renaming ? 6U : 5U)) return 0; + u32 fields = 1 | 2; + if (form == 1) fields |= 4; // Rb a register + if (three || form == 2 || form == 3) fields |= 8; // Rc, or Rb at 64 + return fields; +} +// (hookshot, extended) How many GPRs a field names, where the opcode tells: the 32-bit ALU forms one each, IMAD.WIDE's +// Rd and Rc pairs, CS2R.64's pair; 0 where it does not (a pair or quad by the register's alignment, as before). A +// 32-bit operand taken for a quad reached R239 or the scratch from R236 or R244 and failed whole mesh programs (OoT's +// foliage). +inline u32 FieldSpan(u32 opcode, u32 field) { + if (!extended_renaming) return 0; + const u32 family = opcode & 0x1ff, form = (opcode >> 9) & 7; + if (opcode == 0x805) return field == 0 ? 2 : 0; + if (opcode == 0x308) return 1; + if (opcode == 0x387 || opcode == 0x987 || opcode == 0x983) return field == 1 ? 1 : 0; // (the address register) + if (form < 1 || form > 7) return 0; + if (family == 0x025) return field == 0 || field == 3 ? 2 : 1; + switch (family) { + case 0x010: case 0x011: case 0x012: case 0x016: case 0x019: case 0x023: case 0x024: case 0x027: + case 0x020: case 0x021: case 0x007: case 0x008: case 0x009: case 0x017: case 0x00b: case 0x00c: + return 1; + default: + return 0; + } +} struct LoweredProgram { std::vector instructions; std::vector source_instruction_offsets; u32 source_size{}; u32 required_texture_buffer = ~0U; + // UR28 and up seen without a mirror: lowering again with this many registers below R216 + // reserved for them (even, so pairs stay adjacent) maps them. + u32 high_uniform_registers{}; + // (hookshot) the reserved numbers uniform_register gave, all instructions: what renamed registers' markers avoid + std::array used_reserved{}; + u32 register_map_words{}; // (hookshot: the REGMAP_nx2 prefixes emitted) u32 error_offset{}; std::string_view error; @@ -43,8 +129,11 @@ struct LoweredProgram { // Lower only operations whose semantics are representable by the existing // Maxwell frontend. These are new instructions, not reinterpretations of the // native bytes. Scheduling/control words are added by the environment adapter. -inline LoweredProgram LowerToMaxwell(std::span code) { +inline LoweredProgram LowerToMaxwellWith(std::span code, u32 high_uniforms, + const std::array& avoid = {}, bool survey = false) { LoweredProgram result; + // UR28 and up (when used) live just below R216: UR28 at R(216 - high_uniforms). + const u32 high_uniform_base = 216 - high_uniforms; struct Relocation { u32 output_index; s64 target; }; std::vector relocations; std::vector barrier_tokens; @@ -55,8 +144,9 @@ inline LoweredProgram LowerToMaxwell(std::span code) { bool uses_uniform_bank = false; for (const auto& instruction : code) { const u32 op = instruction.Bits(0, 12); - uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || - op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)); + uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0x499 || op == 0x4a4 || op == 0xc07 || op == 0xc08 || op == 0x896 || op == 0xf60 || op == 0xc11 || op == 0xd08 || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || + op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0xf63 || op == 0xf69 || op == 0xc0b || op == 0x8a5 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)) || op == 0xc0a || + ((op == 0x987 || op == 0x983) && instruction.Bits(91, 1)); } const auto address_of = [](u32 index) { return 0x50U + index * 8 + ((index + 1) / 3) * 8; }; const auto fail = [&](u32 index, std::string_view reason) { @@ -83,6 +173,7 @@ inline LoweredProgram LowerToMaxwell(std::span code) { result.source_instruction_offsets.push_back(static_cast(result.instructions.size())); u32 opcode = insn.Bits(0, 12); if (insn.Bits(12, 4) == 15) continue; // @!PT: never executes (compiler padding) + u32 f16_dest = 255, f16_second = 255, f16_components = 0; std::optional active_link; u32 active_entry = 0; if (main_exited) { @@ -92,32 +183,131 @@ inline LoweredProgram LowerToMaxwell(std::span code) { active_link = call_links[call]; } } - // RET has no destination GPR; bits 16..23 are reserved, not a write to R0. - if (active_link && opcode != 0x950 && insn.Bits(16, 8) == *active_link) + // RET, BRA, NOP and ISETP/FSETP (predicate outputs) have no destination GPR; bits 16..23 + // are not a write to R0. IMAD.WIDE writes a register pair. + const bool no_destination = opcode == 0x950 || opcode == 0x947 || opcode == 0x918 || + (opcode & 0x1ff) == 0x00c || (opcode & 0x1ff) == 0x00b; + const bool pair = opcode == 0x225 || opcode == 0x625 || opcode == 0x825; + if (active_link && !no_destination && + (insn.Bits(16, 8) == *active_link || (pair && insn.Bits(16, 8) + 1 == *active_link))) return fail(index, "native callee overwrites link register"); } - // Scratch values and emulated uniform state occupy reserved high GPRs. - // Reject native accesses that could alias that state. - for (u32 bit : {16U, 24U, 32U, 64U}) { - if (bit == 32 && insn.Bits(9, 3) != 1 && opcode != 0xb60 && opcode != 0xb66) continue; - const u32 reg = insn.Bits(bit, 8); - if (reg >= (uses_uniform_bank ? 216U : 246U) && reg < 255) return fail(index, "native program uses lowering scratch registers"); + // Scratch values and emulated uniform state occupy reserved high GPRs. (hookshot) A native access that could + // alias them makes the instruction's high GPRs extended registers (RegisterMapPrefix): every field that may + // be one, each as the first of four (vectors), the over-counted kept apart from the lowering's own below + const u32 reserved_base = uses_uniform_bank ? high_uniform_base : 246U; + std::array native_high{}; + bool any_high = false, high_in_scratch = false, high_239 = false; + RegisterRun renamed[8]; + u32 nrenamed = 0; + std::array marker_taken{}; + { + const u32 renameable = RenameableFields(opcode); + constexpr u32 bits[4]{16, 24, 32, 64}; + // (a pair or quad starts at an even or a 4-aligned register: an odd one is one register, R4n+2 at most two) + const auto span_of = [](u32 reg) { return (reg & 1) ? 1U : (reg & 2) ? 2U : 4U; }; + for (u32 f = 0; f < 4; ++f) { // what may be native high GPRs, and which are to be renamed + const u32 bit = bits[f], reg = insn.Bits(bit, 8); + // (ISBEWR's data too: not counted, a mesh program's output held in a high GPR was read from the plain register, + // never written, while what computes it wrote the extended one: vertices past the first 32 came out undefined. + // SUST and SURED's data as well, the same field. (amd-fix, Lichii #149) RED's too: a reduction's data in a high GPR + // read the plain register, never written. (Lichii #588) TXD's four gradients (0xb6c) and the global atomic's + // data (0x9a8) too: both are registers at bit 32) + const bool register_field = bit != 32 || insn.Bits(9, 3) == 1 || opcode == 0xb60 || opcode == 0xb66 || + opcode == 0x927 || opcode == 0xb9b || opcode == 0x59f || opcode == 0x983 || + opcode == 0x98e || opcode == 0xb6c || opcode == 0x9a8 || (renameable & (1U << f)); + if (reg >= 255 || reg + 3 < reserved_base) continue; // (a quad below may reach up) + if (register_field && reg >= reserved_base) any_high = true; + } + for (u32 f = 0; f < 4 && any_high; ++f) { + const u32 bit = bits[f], reg = insn.Bits(bit, 8); + if (reg >= 255 || reg + 3 < reserved_base) continue; + const u32 exact = FieldSpan(opcode, f); + const u32 span = exact ? exact : span_of(reg); + if (exact && reg + span <= reserved_base) continue; // (wholly below the reserved registers) + bool reaches_scratch = reg + span > 246 || (reg <= 239 && reg + span > 239); + for (u32 k = 0; k < span && reg + k < 255; ++k) // (or a number a uniform register has somewhere) + reaches_scratch |= avoid[reg + k]; + if ((renameable & (1U << f)) && reaches_scratch && reg >= reserved_base) { + // a marker block, aligned as the register, among the uniform bank's numbers nobody uses here + u32 marker = 255; + for (u32 m = 216; m + span <= 246 && marker == 255; m += span) { + bool ok = true; + for (u32 k = 0; k < span && ok; ++k) + ok = m + k != 239 && !avoid[m + k] && !marker_taken[m + k] && !native_high[m + k]; + if (ok) marker = m; + } + if (marker != 255 && nrenamed < 8) { + bool same = false; // (the same register in two fields: one marker) + for (u32 r = 0; r < nrenamed; ++r) + if (renamed[r].target == reg) { + same = true; + insn.words[bit / 32] = (insn.words[bit / 32] & ~(0xffU << (bit % 32))) | (renamed[r].number << (bit % 32)); + } + if (!same) { + for (u32 k = 0; k < span; ++k) marker_taken[marker + k] = true; + renamed[nrenamed++] = {marker, reg, span}; + insn.words[bit / 32] = (insn.words[bit / 32] & ~(0xffU << (bit % 32))) | (marker << (bit % 32)); + } + continue; + } + } + for (u32 k = 0; k < span && reg + k < 255; ++k) { + if (reg + k < reserved_base) continue; + native_high[reg + k] = true; + high_in_scratch |= reg + k >= 246; + high_239 |= reg + k == 239; + } + } + } + if (!any_high) native_high = {}; + static const bool no_extended = std::getenv("RX_VK_NO_EXTREGS") != nullptr; // (as before them: rejected) + if (any_high && no_extended) return fail(index, "native program uses lowering scratch registers"); + std::array lowering_used{}; // (the reserved numbers uniform_register gave this instruction) + const size_t words_before = result.instructions.size(); + const size_t relocations_before = relocations.size(); + // TLD/TLD4 .F16 (bit 79): the components come packed in pairs, (0, 1) in Rd and (2, 3) in + // the second destination. Lowered as the F32 form into R248/R249 and R246/R247, then packed. + if ((opcode == 0xb66 || opcode == 0xb63) && insn.Bits(79, 2) == 1) { + f16_dest = insn.Bits(16, 8); + f16_second = insn.Bits(64, 8); + f16_components = static_cast(std::popcount(insn.Bits(72, 4))); + if (f16_dest >= 246 || (f16_second != 255 && f16_second >= 246) || insn.Bits(61, 3) == 5 || + (f16_second == 255 && f16_components > 2)) + return fail(index, "native half-precision texture result registers"); + insn.words[2] &= ~(1U << 15); + insn.words[0] = (insn.words[0] & ~0x00ff0000U) | (f16_dest == 255 ? 0x00ff0000U : (248U << 16)); + if (f16_second != 255) insn.words[2] = (insn.words[2] & ~0xffU) | 246U; } // UR0-UR21 live in R216-R237 and UR22-UR27 in R240-R245 (R239 holds the uniform - // predicates), so even/odd pairs stay adjacent. Sites that still check reg < 16 rely on - // UR0-UR15 being R216-R231. - const auto uniform_register = [&](u32 reg) -> u32 { + // predicates), so even/odd pairs stay adjacent; UR28 and up below R216. Sites that + // still check reg < 16 rely on UR0-UR15 being R216-R231. + const auto uniform_register_of = [&](u32 reg) -> u32 { if (reg < 22) return 216 + reg; if (reg < 28) return 218 + reg; if (reg == 63) return 255; + if (reg < 63 && reg - 28 < high_uniforms) return high_uniform_base + (reg - 28); + if (reg < 63) result.high_uniform_registers = (std::max)(result.high_uniform_registers, (reg - 26) & ~1U); return ~0U; }; + const auto uniform_register = [&](u32 reg) -> u32 { + const u32 r = uniform_register_of(reg); + if (r < 255) lowering_used[r] = true; + return r; + }; bool rewritten_uniform = false; bool uniform_guard = false; u32 uniform_output = 7; // ULOP3's UPn output: computed in P(6-n), then merged into R239 if (opcode == 0x2a4 || opcode == 0x8a4 || opcode == 0x891 || opcode == 0x890 || opcode == 0x290 || - opcode == 0x292 || opcode == 0x892 || opcode == 0x291) { - if (insn.Bits(12, 4) != 7 && opcode != 0x292 && opcode != 0x892) + opcode == 0x292 || opcode == 0x892 || opcode == 0x291 || opcode == 0x896 || opcode == 0x8a5 || + opcode == 0x4a4) { // (hookshot: 0x4a4, UIMAD URd, URa, imm, URc, as IMAD 0x424) + // (hookshot) a UPn guard on any of these that writes no predicate (no carry out: bits 81-83 and 84-86 + // PT), as ULOP3's: swapped into P(6-n) around the op (guarded_uniform below) + // (ULEA, 0x291/0x891, has one carry out: its bits 84-86 are no predicate) + const bool lea = opcode == 0x291 || opcode == 0x891; + const bool no_predicate_out = insn.Bits(81, 3) == 7 && (lea || insn.Bits(84, 3) == 7); + // (hookshot) and a guarded ULEA with a carry out: the LEA below starts that UPn from R239 when guarded + if (insn.Bits(12, 4) != 7 && opcode != 0x292 && opcode != 0x892 && !no_predicate_out && !lea) return fail(index, "native uniform arithmetic predication"); uniform_guard = insn.Bits(12, 4) != 7; rewritten_uniform = true; @@ -137,10 +327,13 @@ inline LoweredProgram LowerToMaxwell(std::span code) { uniform_output = insn.Bits(81, 3); insn.words[2] = (insn.words[2] & ~(7U << 17)) | ((6 - uniform_output) << 17); } - opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x8a4 ? 0x824 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810 - : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211 : 0x210; + opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x8a4 ? 0x824 : opcode == 0x4a4 ? 0x424 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810 + : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211 + : opcode == 0x896 ? 0x816 : opcode == 0x8a5 ? 0x825 : 0x210; } - const bool uniform_predication = opcode == 0xabb || opcode == 0xab9 || opcode == 0xc82 || opcode == 0x882; + // (hookshot) and the uniform shifts, whose guard is swapped in the same way + const bool uniform_predication = opcode == 0xabb || opcode == 0xab9 || opcode == 0xc82 || opcode == 0x882 || + opcode == 0x899 || opcode == 0x299 || opcode == 0x499; const u32 native_predicate = insn.Bits(12, 4); const bool guarded_uniform = (uniform_predication || uniform_guard) && native_predicate != 7; // A uniform guard UPn lives in R239 bit 6-n and is swapped into P(6-n) for the op. @@ -163,6 +356,13 @@ inline LoweredProgram LowerToMaxwell(std::span code) { }; std::string_view operand_error; const auto emit = [&](u64 word) { result.instructions.push_back(word); }; + // Bound Maxwell texture instructions share one descriptor constant buffer per program. + const auto bind_texture_buffer = [&](u32 binding) { + if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + return false; + result.required_texture_buffer = binding; + return true; + }; // P(guard slot) while a uniform guard is swapped in; P6 for lowerings that use it as // scratch (R2UR, MATCH, HFMA2.RELU), which save and restore it with the defaults below. const u32 guard_mask = guard_slot < 7 ? 1U << guard_slot : 0U; @@ -252,6 +452,42 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } return materialize(kind, reg, 250 + which, floating && abs, floating && neg); }; + // (hookshot) Ampere SHF in general: (hi:lo) shifted by an immediate or a register, bits 73-74 the type (0 S64, + // 1 U64, 2 S32, 3 U32), 75 wrap, 76 right, 80 the high word of the result. Maxwell's SHF.R gives the low + // word of (hi:lo) >> s and SHF.L the high word of (hi:lo) << s (max shift at 37-38: U32 0, U64 2, S64 3; + // wrap at 50, as Eden's decoder reads them); the other two words are a plain SHR of hi or SHL of lo + // (.W at 39). False: a shift by an immediate past the type's width. + const auto funnel = [&](u32 dst, u32 lo, u32 hi, bool shift_in_register, u32 shift, u32 type, bool right, + bool hi_word, bool wrap) -> bool { + const bool wide = type < 2, sign = type == 0 || type == 2; + if (!shift_in_register && shift >= (wide ? 64U : 33U)) return false; + const u64 amount = u64{shift} << 20, w39 = u64{wrap} << 39; + if (right != hi_word) { + // (hookshot, from Lichii) an immediate's bits 20-38 are all the amount: the clamp at 37-38 made every shift 63 + if (!shift_in_register && wide && shift >= 32) { + emit((right ? 0x3828000000000000ULL : 0x3848000000000000ULL) | predicate | dst | + (u64{right ? hi : lo} << 8) | (u64{shift - 32} << 20) | (u64{right && sign} << 48)); + return true; + } + const u64 op = shift_in_register ? (right ? 0x5cf8000000000000ULL : 0x5bf8000000000000ULL) + : (right ? 0x38f8000000000000ULL : 0x36f8000000000000ULL); + const u64 max_shift = !shift_in_register || !wide ? 0U : sign ? 3U : 2U; + emit(op | predicate | dst | (u64{lo} << 8) | amount | (max_shift << 37) | (u64{hi} << 39) | + (u64{wrap} << 50)); + return true; + } + if (!shift_in_register && shift >= 32) { // (the word shifted out entirely) + if (right && sign) emit(0x3828000000000000ULL | predicate | dst | (u64{hi} << 8) | (u64{31} << 20) | + (u64{1} << 48)); + else emit(0x5c98000000000000ULL | predicate | dst | (u64{255} << 20) | (u64{15} << 39)); + return true; + } + if (right) emit((shift_in_register ? 0x5c28000000000000ULL : 0x3828000000000000ULL) | predicate | dst | + (u64{hi} << 8) | amount | w39 | (u64{sign} << 48)); + else emit((shift_in_register ? 0x5c48000000000000ULL : 0x3848000000000000ULL) | predicate | dst | + (u64{lo} << 8) | amount | w39); + return true; + }; struct HalfSource { u32 reg, swizzle; bool absolute, negate; }; const auto half_source = [&](u32 which) -> HalfSource { if (which == 0) return {insn.Bits(24, 8), insn.Bits(74, 2), @@ -466,6 +702,20 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(base(0x5c98000000000000ULL) | (u64{15} << 39) | (u64{insn.Bits(64, 8)} << 20)); break; } + if (insn.Bits(24, 8) == 255 && insn.words[1] == 255 && (insn.words[2] & ~0x07800affU) == 0x000e0400 && + (!rewritten_uniform || insn.Bits(87, 3) == 7)) { + // IMAD.X RZ, RZ, c, Pn: the high word of a 64-bit add (bit 75 inverts c). + u32 c = insn.Bits(64, 8); + mov_imm(250, 1); + emit(0x5ca0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | + (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42)); + if (insn.Bits(75, 1)) { + emit(0x0400000000000000ULL | predicate | 251 | (u64{c} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53)); + c = 251; + } + emit(base(0x5c10000000000000ULL) | (u64{c} << 8) | (u64{250} << 20)); + break; + } if ((insn.words[2] & ~0x2ffU) != 0x078e0000 || (insn.words[1] & ~0xffU)) return fail(index, "native integer register multiply mode"); multiply_low(insn.Bits(24, 8), insn.Bits(32, 8), insn.Bits(64, 8), false); @@ -496,16 +746,87 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(0x5c10000000070000ULL | d_low | (u64{250} << 8) | (u64{255} << 20)); break; } - case 0x827: { // IMAD.HI.U32, captured zero addend - if (insn.words[2] != 0x078e00ff) return fail(index, "native high multiply mode"); - const u32 a = insn.Bits(24, 8), b = insn.words[1]; + case 0x226: { // IDP.2A.LO.U16.U8: a.lo16 * b.byte0 + a.hi16 * b.byte1 + c + if ((insn.words[2] & ~0xffU) != 0x1000 || (insn.words[1] & ~0xffU)) + return fail(index, "native packed dot-product mode"); + const u32 a = insn.Bits(24, 8), b = insn.Bits(32, 8), c = insn.Bits(64, 8); + for (u32 component = 0; component < 2; ++component) { + emit(0x3828000000000000ULL | predicate | (250 + component) | (u64{b} << 8) | (u64{component * 8} << 20)); + emit(0x0400000000000000ULL | predicate | (250 + component) | (u64{250 + component} << 8) | (u64{0xff} << 20)); + } + xmad(252, a, 250, c, false, false, 0, false); + xmad(static_cast(dest), a, 251, 252, true, false, 0, false); + break; + } + case 0x225: case 0x227: case 0x625: case 0x825: case 0x827: case 0xa27: { + // IMAD.HI Rd / IMAD.WIDE Rd:Rd+1 = a * b + c:c+1 (.U32 or signed, bit 73), with an optional + // carry-out (bits 81-83). b is a register, an immediate (0x8xx) or a cbuf (0xa27); 0x625 takes + // b from bits 64-71 and adds a cbuf pair. + const bool wide = opcode == 0x225 || opcode == 0x625 || opcode == 0x825; + const bool immediate = opcode == 0x825 || opcode == 0x827, signed_product = insn.Bits(73, 1); + const u32 c = opcode == 0x625 ? 255U : insn.Bits(64, 8), carry_output = insn.Bits(81, 3); + if ((insn.words[2] & ~0x000e02ffU) != 0x07800000 || + ((opcode == 0x225 || opcode == 0x227) && (insn.words[1] & ~0xffU)) || + ((opcode == 0xa27 || opcode == 0x625) && (!valid_cbuf() || insn.Bits(62, 2))) || + (opcode == 0x625 && insn.Bits(38, 16) > 0xfff8) || (c != 255 && (c & 1)) || + (wide && (dest == 255 || (dest & 1))) || (signed_product && carry_output != 7)) + return fail(index, "native high multiply mode"); + const u32 a = insn.Bits(24, 8); + const u32 b = immediate ? insn.words[1] : opcode == 0xa27 ? 254U : insn.Bits(opcode == 0x625 ? 64 : 32, 8); + if (opcode == 0xa27) emit(0x4c98000000000000ULL | predicate | 254 | (u64{15} << 39) | cbuf()); + if (signed_product) { + // The signed high word is the unsigned one minus (a < 0 ? b : 0) + (b < 0 ? a : 0), + // which R253 holds (R252 is free until the multiplication). + const u32 b_register = immediate ? 254U : b; + if (immediate) mov_imm(254, b); + emit(0x3828000000000000ULL | predicate | 253 | (u64{a} << 8) | (u64{31} << 20) | (u64{1} << 48)); + emit(0x5c40000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{b_register} << 20) | (u64{7} << 48)); + emit(0x3828000000000000ULL | predicate | 252 | (u64{b_register} << 8) | (u64{31} << 20) | (u64{1} << 48)); + emit(0x5c40000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{a} << 20) | (u64{7} << 48)); + emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{252} << 20)); + } // Base-2^16 long multiplication. Each intermediate sum fits U32. - xmad(250, a, b, 255, false, false, 0, true); - xmad(251, a, b, 250, true, false, 2, true); - xmad(250, a, b, 251, false, true, 1, true); - xmad(252, a, b, 251, true, true, 2, true); + xmad(250, a, b, 255, false, false, 0, immediate); + xmad(251, a, b, 250, true, false, 2, immediate); + xmad(250, a, b, 251, false, true, 1, immediate); + xmad(252, a, b, 251, true, true, 2, immediate); emit(0x3828000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{16} << 20)); - emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20)); + if (signed_product) + emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{253} << 20) | (u64{1} << 48)); + if (!wide && c == 255 && carry_output == 7) { + emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20)); + break; + } + // High word in R253 and low word in R250 (as UIMAD.WIDE.U32), then c:c+1 is added. + emit(0x5c10000000000000ULL | predicate | 253 | (u64{250} << 8) | (u64{252} << 20)); + xmad(250, a, b, 255, false, false, 0, immediate); + xmad(251, a, b, 255, true, false, 0, immediate); + xmad(251, a, b, 251, false, true, 0, immediate); + emit(0x5c18000000000000ULL | predicate | 250 | (u64{251} << 8) | (u64{250} << 20) | (u64{16} << 39)); + u32 c_low = c, c_high = c == 255 ? 255U : c + 1; + if (opcode == 0x625) { + emit(0x4c98000000000000ULL | predicate | 252 | (u64{15} << 39) | cbuf()); + emit(0x4c98000000000000ULL | predicate | 254 | (u64{15} << 39) | (cbuf() + (u64{1} << 20))); + c_low = 252; + c_high = 254; + } + const u32 high = wide ? static_cast(dest) + 1 : static_cast(dest); + emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{c_low} << 20) | (u64{1} << 47)); + if (carry_output == 7) { + emit(0x5c10000000000000ULL | predicate | high | (u64{253} << 8) | (u64{c_high} << 20) | (u64{1} << 43)); + if (wide) emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{255} << 20)); + break; + } + // Maxwell IADD has no .X with .CC: R251 takes the low carry, R254 counts the high word's. + emit(0x5c10000000000000ULL | predicate | 251 | (u64{255} << 8) | (u64{255} << 20) | (u64{1} << 43)); + emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{c_high} << 20) | (u64{1} << 47)); + emit(0x5c10000000000000ULL | predicate | 254 | (u64{255} << 8) | (u64{255} << 20) | (u64{1} << 43)); + emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{251} << 20) | (u64{1} << 47)); + emit(0x5c10000000000000ULL | predicate | 254 | (u64{254} << 8) | (u64{255} << 20) | (u64{1} << 43)); + emit(0x5c98000000000000ULL | predicate | high | (u64{253} << 20) | (u64{15} << 39)); + if (wide) emit(base(0x5c98000000000000ULL) | (u64{250} << 20) | (u64{15} << 39)); + emit(0x5b60000000000000ULL | predicate | 7 | (u64{carry_output} << 3) | (u64{254} << 8) | + (u64{255} << 20) | (u64{7} << 39) | (u64{5} << 49)); break; } case 0xa24: { // IMAD, constant-buffer multiplier @@ -547,6 +868,21 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } case 0x824: { // Captured low-word IMAD forms; bit 75 negates the addend. const bool negate_c = insn.Bits(75, 1) != 0; + if (insn.words[1] == 1 && (insn.words[2] & ~(0x07800800U | 0xffU)) == 0x000e0600U) { + // IMAD.X Rd, Ra, 1, Rc, Pn: Ra + Rc plus the carry predicate in bits 87-90 (~Rc with bit 75). + u32 c = insn.Bits(64, 8); + mov_imm(252, 1); + emit(0x5ca0000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{255} << 20) | + (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42)); + if (negate_c) { + emit(0x0400000000000000ULL | predicate | 251 | (u64{c} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53)); + c = 251; + } + emit(0x5c10000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) | + (u64{c} << 20)); + emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20)); + break; + } if (insn.words[1] == 1 && (insn.words[2] & 0xfffff700U) == 0x078e0200) { emit(base(0x5c10000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(64, 8)} << 20) | (u64{negate_c} << 48)); @@ -588,7 +924,7 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } case 0x886: { // VOTEU ballot and independent uniform predicate output. const u32 output = insn.Bits(81, 3), target = insn.Bits(16, 8); - if ((target >= 16 && target != 63) || insn.words[1] || + if ((target != 63 && uniform_register(target) == ~0U) || insn.words[1] || // (hookshot: any UR) insn.Bits(72, 2) > 2 || native_predicate != 7) return fail(index, "native uniform vote mode"); const u32 temporary = output == 7 ? 7 : 6 - output; @@ -614,8 +950,15 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(0x5c30000000070000ULL | 250 | (u64{250} << 20)); emit(0xef10000000070000ULL | 251 | (u64{insn.Bits(24, 8)} << 8) | (u64{250} << 20) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48)); - emit(0x5ca0000000070000ULL | target | (u64{251} << 8) | - (u64{target} << 20) | (u64{6} << 39)); + if (native_predicate == 7) { + // Every lane executing it is in the ballot, so no lane-less fallback can be taken. + // Leaving the old value selectable would make it a source of the result for the + // texture and global memory tracking (a handle traced to the uniform's last load). + emit(0x5c98000000070000ULL | target | (u64{15} << 39) | (u64{251} << 20)); + } else { + emit(0x5ca0000000070000ULL | target | (u64{251} << 8) | + (u64{target} << 20) | (u64{6} << 39)); + } restore_predicate(); break; } @@ -673,16 +1016,18 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } break; } - case 0x882: { // UMOV immediate. - if (dest >= 16 || insn.words[2]) return fail(index, "native uniform immediate move mode"); - mov_imm(static_cast(dest) + 216, insn.words[1]); + case 0x882: { // UMOV immediate. (hookshot: any UR, through uniform_register) + const u32 d = uniform_register(dest); + if (d == ~0U || insn.words[2]) return fail(index, "native uniform immediate move mode"); + if (d != 255) mov_imm(d, insn.words[1]); break; } case 0xc82: { // UMOV UR, UR (or URZ), including uniform execution predicates. const u32 source = insn.words[1] < 64 ? uniform_register(insn.words[1]) : ~0U; - if (dest >= 16 || source == ~0U || insn.words[2] != 0x08000000) + const u32 d = uniform_register(dest); // (hookshot: any UR) + if (d == ~0U || d == 255 || source == ~0U || insn.words[2] != 0x08000000) return fail(index, "native uniform move mode"); - emit(0x5c98000000000000ULL | predicate | (dest + 216) | + emit(0x5c98000000000000ULL | predicate | d | (u64{source} << 20) | (u64{15} << 39)); break; } @@ -691,13 +1036,15 @@ inline LoweredProgram LowerToMaxwell(std::span code) { const u32 width = insn.Bits(73, 3); const u32 target = uniform_register(uniform_dest); const u32 index_target = opcode == 0xabb ? uniform_register(index_reg) : 255U; - if (target == ~0U || target == 255 || (width != 4 && width != 5) || + // (hookshot) U8/S8/U16/S16 too (0-3, Maxwell LDC's sizes), each aligned to its own size + const u32 align = width >= 5 ? 8U : width == 4 ? 4U : width >= 2 ? 2U : 1U; + if (target == ~0U || target == 255 || width > 5 || (width == 5 && ((uniform_dest & 1) || uniform_register(uniform_dest + 1) != target + 1)) || - bank() >= 18 || insn.Bits(38, 16) % 4 || insn.Bits(62, 2) || + bank() >= 18 || (opcode == 0xab9 && insn.Bits(38, 16) % (align < 4 ? align : 4U)) || insn.Bits(62, 2) || (insn.words[2] & ~0xe00U) != (opcode == 0xab9 ? 0U : 0x08000000U) || index_target == ~0U) return fail(index, "native uniform constant load mode"); - if (opcode == 0xab9 && width == 4) { + if (opcode == 0xab9 && width == 4 && insn.Bits(38, 16) % 4 == 0) { emit(0x4c98000000000000ULL | predicate | target | (u64{15} << 39) | cbuf()); } else { emit(0xef90000000000000ULL | predicate | target | @@ -708,36 +1055,80 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } case 0x899: { // USHF.L.U32; accept a zero word outside the selected half. const u32 dst = insn.Bits(16, 8), low = insn.Bits(24, 8), high = insn.Bits(64, 8); - if (insn.words[2] == (0x08011400U | high) && low == 63 && high < 16 && - dst < 16 && native_predicate == 7 && insn.words[1] <= 32) { - emit(0x3828000000000000ULL | predicate | (dst + 216) | - (u64{high + 216} << 8) | (u64{insn.words[1]} << 20) | (u64{1} << 48)); + const u32 dst_reg = uniform_register(dst), low_reg = uniform_register(low); + const u32 high_reg = uniform_register(high); + const bool dst_ok = dst != 63 && dst_reg != ~0U; + const bool low_ok = low != 63 && low_reg != ~0U, high_ok = high != 63 && high_reg != ~0U; + if (insn.words[2] == (0x08011400U | high) && low == 63 && high_ok && + dst_ok && (native_predicate == 7 || guarded_uniform) && insn.words[1] <= 32) { + emit(0x3828000000000000ULL | predicate | dst_reg | + (u64{high_reg} << 8) | (u64{insn.words[1]} << 20) | (u64{1} << 48)); break; } // Logical right shifts of one register (bit 76 right, bit 80 high word, bits 73-74 // U32): USHF.R.U32 URd, URa, n, URZ and USHF.R.U32.HI URd, URZ, n, URb. - const bool right_low = insn.words[2] == 0x0800163f && low < 16; - const bool right_high = insn.words[2] == (0x08011600U | high) && low == 63 && high < 16; - if ((right_low || right_high) && dst < 16 && native_predicate == 7 && insn.words[1] <= 32) { - emit(0x3828000000000000ULL | predicate | (dst + 216) | - (u64{(right_high ? high : low) + 216} << 8) | (u64{insn.words[1]} << 20)); + const bool right_low = insn.words[2] == 0x0800163f && low_ok; + const bool right_high = insn.words[2] == (0x08011600U | high) && low == 63 && high_ok; + if ((right_low || right_high) && dst_ok && (native_predicate == 7 || guarded_uniform) && insn.words[1] <= 32) { + emit(0x3828000000000000ULL | predicate | dst_reg | + (u64{right_high ? high_reg : low_reg} << 8) | (u64{insn.words[1]} << 20)); break; } - const bool high_result = insn.words[2] == (0x08010600U | high) && low == 63 && high < 16; - const bool low_result = insn.words[2] == 0x0800063f && low < 16; - if (dst >= 16 || native_predicate != 7 || insn.words[1] > 32 || (!high_result && !low_result)) + const bool high_result = insn.words[2] == (0x08010600U | high) && low == 63 && high_ok; + const bool low_result = insn.words[2] == 0x0800063f && low_ok; + if (dst_ok && (native_predicate == 7 || guarded_uniform) && !high_result && !low_result && !insn.Bits(75, 1) && + (insn.words[2] & ~0x000116ffU) == 0x08000000U) { + // (hookshot) the general funnel shift by an immediate, both words: bits 73-74 the type (0 S64, + // 1 U64, 2 S32, 3 U32), 76 right, 80 the high word of the result. Maxwell's SHF.R gives the + // low word of (high:low) >> s and SHF.L the high word of (high:low) << s (bits 37-38 the + // maximum shift, U64 2 / S64 3); the other two words are plain shifts of one source. + const u32 type = insn.Bits(73, 2), shift = insn.words[1]; + const bool wide = type < 2, sign = type == 0 || type == 2, right = insn.Bits(76, 1) != 0; + const bool hi = insn.Bits(80, 1) != 0; + const u32 lo_r = low == 63 ? 255U : low_reg, hi_r = high == 63 ? 255U : high_reg; + const bool lo_valid = low == 63 || low_ok, hi_valid = high == 63 || high_ok; + if (lo_valid && hi_valid && shift < (wide ? 64U : 33U)) { + const u64 max_shift = 0; // (hookshot, from Lichii: not with an immediate, as in funnel) + if (wide && shift >= 32 && right != hi) { + emit((right ? 0x3828000000000000ULL : 0x3848000000000000ULL) | predicate | dst_reg | + (u64{right ? hi_r : lo_r} << 8) | (u64{shift - 32} << 20) | (u64{right && sign} << 48)); + break; + } + if (right && !hi) { + emit(0x38f8000000000000ULL | predicate | dst_reg | (u64{lo_r} << 8) | (u64{shift} << 20) | + (max_shift << 37) | (u64{hi_r} << 39)); + break; + } + if (!right && hi) { + emit(0x36f8000000000000ULL | predicate | dst_reg | (u64{lo_r} << 8) | (u64{shift} << 20) | + (max_shift << 37) | (u64{hi_r} << 39)); + break; + } + if (shift < 32) { + if (right) emit(0x3828000000000000ULL | predicate | dst_reg | (u64{hi_r} << 8) | + (u64{shift} << 20) | (u64{sign} << 48)); + else emit(0x3848000000000000ULL | predicate | dst_reg | (u64{lo_r} << 8) | (u64{shift} << 20)); + break; + } + } + } + if (!dst_ok || (native_predicate != 7 && !guarded_uniform) || insn.words[1] > 32 || (!high_result && !low_result)) return fail(index, "native uniform shift mode"); - emit(0x3848000000000000ULL | predicate | (dst + 216) | - (u64{(high_result ? high : low) + 216} << 8) | (u64{insn.words[1]} << 20)); + emit(0x3848000000000000ULL | predicate | dst_reg | + (u64{high_result ? high_reg : low_reg} << 8) | (u64{insn.words[1]} << 20)); break; } - case 0xb82: { // LDC.64 indexed byte offset - const u32 width = insn.Bits(73, 3); - if ((width != 4 && width != 5) || insn.Bits(78, 2) || insn.Bits(91, 1) || + case 0xb82: { // LDC.64 indexed byte offset; bits 78-79 the mode as Maxwell's (2 = IS) + // .IS: Ra + offset addresses all constant buffers as one space, the buffer in its high + // half (Kirby Air Riders' vertex shaders), which Eden's Maxwell LDC implements. + const u32 width = insn.Bits(73, 3), mode = insn.Bits(78, 2); + // (hookshot) U8/S8/U16/S16 (0-3) too: Maxwell's LDC has the same size codes, and Eden's decoder + // implements them (OoT's fragment and mesh programs read byte tables from banks 8 and 9). + if (width > 5 || (mode != 0 && mode != 2) || insn.Bits(91, 1) || bank() >= 18 || (width == 5 && ((dest & 1) || dest >= 254))) return fail(index, "native indexed constant load mode"); emit(base(0xef90000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | - (u64{insn.Bits(38, 16)} << 20) | (u64{bank()} << 36) | (u64{width} << 48)); + (u64{insn.Bits(38, 16)} << 20) | (u64{bank()} << 36) | (u64{mode} << 44) | (u64{width} << 48)); break; } case 0xf89: { // SHFL with immediate lane and segmentation/clamp mask, any mode. @@ -759,33 +1150,39 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{insn.Bits(64, 8)} << 20) | (u64{maxwell} << 28)); break; } - case 0xb6c: { // TXD 2D or ARRAY_2D, four channels; the two destination pairs anywhere. + case 0xb6c: { // TXD 2D or ARRAY_2D, any channels; the two destination pairs anywhere. const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 coordinates = insn.Bits(24, 8), derivatives = insn.Bits(32, 8); // Ampere dimensions: 1 = 2D, 5 = ARRAY_2D; Maxwell TXD's are 2 and 3. The coordinates // (x, y[, layer]) and the four gradients sit in the same registers for both. const u32 dimension = insn.Bits(61, 3); const u32 high = insn.Bits(64, 8); // destination of the third and fourth channels - if ((insn.words[2] & ~0xffU) != 0x001e0f00 || insn.Bits(59, 2) || + // Bit 90 is forwarded below (Maxwell's NODEP); Splatoon Raiders sets it. Bits 72-75 are + // the channel mask: the first two enabled channels go to Rd, Rd+1, the others to the + // second destination (RZ drops them; a third channel alone may be in an odd register). + const u32 mask = insn.Bits(72, 4), channels = static_cast(std::popcount(mask)); + if ((insn.words[2] & ~(0xfffU | (1U << 26))) != 0x001e0000 || mask == 0 || insn.Bits(59, 2) || (dimension != 1 && dimension != 5) || binding >= 18 || offset > 0x1fff || - (dest & 1) || dest >= 248 || (high & 1) || (high >= 248 && high != 255) || + (channels > 1 && (dest & 1)) || dest >= 248 || (channels <= 2 && high != 255) || + (high != 255 && ((channels > 3 && (high & 1)) || high >= 248)) || coordinates >= 248 || derivatives >= 247) return fail(index, "native gradient texture mode"); if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native texture descriptor buffer conflict"); result.required_texture_buffer = binding; - // Maxwell writes four consecutive registers: when the second pair is elsewhere (Splatoon - // Raiders: R12/R18, R18/R44), write them to scratch and move each pair into place. - const bool consecutive = high == dest + 2; + // Maxwell writes the channels to consecutive registers: when the second destination + // doesn't follow Rd (Splatoon Raiders: R12/R18, R18/R44), write them to scratch and move + // each into place. + const bool consecutive = channels <= 2 || high == dest + 2; const u64 target = consecutive ? dest : 248; emit(0xde00000000000000ULL | predicate | target | (u64{coordinates} << 8) | - (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{15} << 31) | + (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{mask} << 31) | (u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); if (!consecutive) { - for (u32 i = 0; i < 2; ++i) { - emit(0x5c98000000000000ULL | predicate | (dest + i) | (u64{15} << 39) | (u64{248 + i} << 20)); - if (high != 255) - emit(0x5c98000000000000ULL | predicate | (high + i) | (u64{15} << 39) | (u64{250 + i} << 20)); + for (u32 i = 0; i < channels; ++i) { + if (i >= 2 && high == 255) break; + const u32 to = i < 2 ? static_cast(dest) + i : high + (i - 2); + emit(0x5c98000000000000ULL | predicate | to | (u64{15} << 39) | (u64{248 + i} << 20)); } } break; @@ -807,6 +1204,23 @@ inline LoweredProgram LowerToMaxwell(std::span code) { break; } case 0x98e: { // RED.E.{ADD..XOR}.STRONG.GPU U32 (op in bits 87-89), register address. + { // (hookshot) any type Maxwell's RED has (bits 73-75: U32 0, S32 1, U64 2, F32 3, its bits 20-22) and a + // register address with a signed offset, or a uniform one ([URa + offset]) + const u32 type = insn.Bits(73, 3); + const u32 rest = insn.words[2] & ~(0x0380003fU | 0x00000e00U); + const bool uniform_address = rest == 0x0c10e100 && insn.Bits(24, 8) == 255; + const s32 offset = static_cast(insn.Bits(40, 24) << 8) >> 8; + if ((type != 0 || insn.Bits(40, 24)) && type <= 3 && (uniform_address || rest == 0x0010e100) && + offset >= -0x80000 && offset <= 0x7ffff && (type != 3 || insn.Bits(87, 3) == 0)) { + const u32 address = uniform_address ? uniform_register(insn.Bits(64, 6)) : insn.Bits(24, 8); + if (address != ~0U && (address == 255 || !(address & 1))) { + emit(0xebf8000000000000ULL | predicate | insn.Bits(32, 8) | (u64{address} << 8) | + (u64{type} << 20) | (u64{insn.Bits(87, 3)} << 23) | + (u64{static_cast(offset) & 0xfffff} << 28) | (u64{1} << 48)); + break; + } + } + } if ((insn.words[2] & ~0x0380003fU) == 0x0c10e100 && insn.Bits(24, 8) == 255) { // [URa + offset]: a uniform 64-bit address with a signed byte offset. const u32 address = uniform_register(insn.Bits(64, 6)); @@ -824,70 +1238,109 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(87, 3)} << 23) | (u64{1} << 48)); break; } - case 0x2bd: // UFLO.U32, the highest set-bit index (.SH, bit 74: its shift amount). - if (dest >= 16 || insn.words[1] >= 16 || (insn.words[2] & ~0x400U) != 0x080e0000 || native_predicate != 7) + case 0x2bd: { // UFLO.U32, the highest set-bit index (.SH, bit 74: its shift amount). (hookshot: any UR) + const u32 d = uniform_register(dest), a = insn.words[1] < 64 ? uniform_register(insn.words[1]) : ~0U; + if (d == ~0U || d == 255 || a == ~0U || (insn.words[2] & ~0x400U) != 0x080e0000 || native_predicate != 7) return fail(index, "native uniform leading bit mode"); - emit(0x5c30000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 20) | - (u64{insn.Bits(74, 1)} << 41)); + emit(0x5c30000000000000ULL | predicate | d | (u64{a} << 20) | (u64{insn.Bits(74, 1)} << 41)); break; - case 0x2bf: // UPOPC, a uniform register's population count. - if (dest >= 16 || insn.words[1] >= 16 || insn.words[2] != 0x08000000 || native_predicate != 7) - return fail(index, "native uniform population count mode"); - emit(0x5c08000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 20)); - break; - case 0x2be: // UBREV, as a bit-reversing zero shift like BREV. - if (dest >= 16 || insn.words[1] >= 16 || insn.words[2] != 0x08000000 || native_predicate != 7) + } + case 0x2be: { // UBREV, as a bit-reversing zero shift like BREV (hookshot: any UR, through uniform_register) + const u32 d = uniform_register(dest), a = insn.words[1] < 64 ? uniform_register(insn.words[1]) : ~0U; + if (d == ~0U || d == 255 || a == ~0U || insn.words[2] != 0x08000000 || native_predicate != 7) return fail(index, "native uniform bit reverse mode"); - emit(0x3828000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 8) | - (u64{1} << 39) | (u64{1} << 40)); + emit(0x3828000000000000ULL | predicate | d | (u64{a} << 8) | (u64{1} << 39) | (u64{1} << 40)); break; - case 0x3a1: { // MATCH.ALL.U64 with only the all-equal predicate retained. + } + case 0x2bf: { // UPOPC, as POPC of the uniform's register (hookshot: any UR) + const u32 d = uniform_register(dest), a = insn.words[1] < 64 ? uniform_register(insn.words[1]) : ~0U; + if (d == ~0U || d == 255 || a == ~0U || insn.words[2] != 0x08000000 || native_predicate != 7) + return fail(index, "native uniform population count mode"); + emit(0x5c08000000000000ULL | predicate | d | (u64{a} << 20)); + break; + } + case 0x3a1: { // MATCH.ALL (.U32, or .U64 on bit 73) with only the all-equal predicate retained. const u32 source = insn.Bits(24, 8), target = insn.Bits(81, 3); - if (dest != 255 || source >= 254 || (source & 1) || target >= 6 || - native_predicate != 7 || insn.words[1] || (insn.words[2] & ~0xe0000U) != 0x200) + const u32 words = insn.Bits(73, 1) ? 2 : 1; + const u32 scratch = target == 6 ? 5U : 6U; // the comparison, saved around the match + if (dest != 255 || source >= 254 || (words == 2 && (source & 1)) || target == 7 || + native_predicate != 7 || insn.words[1] || (insn.words[2] & ~0xe0200U) != 0) return fail(index, "native warp match mode"); - save_predicate(); + save_predicate(1U << scratch); + // Compare every lane with the lowest active one, then require all to agree. emit(0x50d8000000070000ULL | 250 | (u64{7} << 39) | (u64{7} << 45) | (u64{1} << 48)); emit(0x5c10000000070000ULL | 251 | (u64{250} << 8) | (u64{255} << 20) | (u64{1} << 49)); emit(0x5c40000000070000ULL | 250 | (u64{250} << 8) | (u64{251} << 20) | (u64{7} << 48)); emit(0x5c30000000070000ULL | 250 | (u64{250} << 20)); - for (u32 word = 0; word < 2; ++word) + for (u32 word = 0; word < words; ++word) emit(0xef10000000070000ULL | (251 + word) | (u64{source + word} << 8) | (u64{250} << 20) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48)); - emit(0x5b60000000070000ULL | 7 | (u64{6} << 3) | (u64{source} << 8) | + emit(0x5b60000000070000ULL | 7 | (u64{scratch} << 3) | (u64{source} << 8) | (u64{251} << 20) | (u64{7} << 39) | (u64{2} << 49)); - emit(0x5b60000000070000ULL | 7 | (u64{6} << 3) | (u64{source + 1} << 8) | - (u64{252} << 20) | (u64{6} << 39) | (u64{2} << 49)); - emit(0x50d8000000070000ULL | 255 | (u64{6} << 39) | (u64{target} << 45)); - restore_predicate(); - break; - } - case 0x3c4: { // REDUX.SUM (modulo 2^32) or .MAX (unsigned) over the actual active lanes. + if (words == 2) + emit(0x5b60000000070000ULL | 7 | (u64{scratch} << 3) | (u64{source + 1} << 8) | + (u64{252} << 20) | (u64{scratch} << 39) | (u64{2} << 49)); + emit(0x50d8000000070000ULL | 255 | (u64{scratch} << 39) | (u64{target} << 45)); + restore_predicate(1U << scratch); + break; + } + case 0x80d: { // CSMTEST: a test of shading-mode state Eden does not model, taken as false. + // Kirby Air Riders' vertex-side shaders run "CSMTEST.A.FCSM_TR P0" then "@P0 EXIT": + // never leave early. Outputs of PT discard the result. + const u32 first = insn.Bits(81, 3), second = insn.Bits(84, 3); + if (insn.Bits(87, 4) != 7 || insn.Bits(16, 16)) + return fail(index, "native CSM test mode"); + for (const u32 target : {first, second}) + if (target != 7) + emit(0x5090000000000000ULL | predicate | (u64{target} << 3) | 7 | (u64{7} << 12) | + (u64{1} << 15) | (u64{7} << 29) | (u64{7} << 39)); + break; + } + case 0x3c4: { // REDUX.SUM (modulo 2^32), .MIN/.MAX (unsigned), .OR or .XOR over the actual active lanes. const u32 source = insn.Bits(24, 8), target = uniform_register(static_cast(dest)); - const bool maximum = insn.words[2] == 0x14000; + // (hookshot) bit 73: .S32. The sum, OR and XOR are the same; MIN and MAX are taken unsigned on the values + // with their sign bits flipped, and the result flipped back + const u32 mode = insn.words[2] & ~0x200U; + const bool maximum = mode == 0x14000, minimum = mode == 0x10000; + const bool flip = insn.Bits(73, 1) && (maximum || minimum); + // OR and XOR: inactive lanes contribute zero, as for the sum. Maxwell LOP: 1 = OR, 2 = XOR. + const u32 logical = mode == 0x4000 ? 1U : mode == 0x8000 ? 2U : 0U; if (target == ~0U || target == 255 || insn.words[1] || - (insn.words[2] != 0xc000 && !maximum) || native_predicate != 7) + (mode != 0xc000 && !maximum && !minimum && !logical) || native_predicate != 7) return fail(index, "native warp sum mode"); + const auto flip_sign = [&](u32 reg) { // (LOP32I.XOR reg, 0x80000000) + emit(0x0400000000070000ULL | reg | (u64{reg} << 8) | (u64{0x80000000U} << 20) | (u64{2} << 53)); + }; emit(0x50d8000000070000ULL | 250 | (u64{7} << 39) | (u64{7} << 45) | (u64{1} << 48)); - mov_imm(target, 0); + mov_imm(target, minimum ? ~0U : 0U); for (u32 lane = 0; lane < 32; ++lane) { emit(0xef10000000070000ULL | 251 | (u64{source} << 8) | (u64{lane} << 20) | (u64{1} << 28) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48)); + if (flip) flip_sign(251); emit(0x3828000000070000ULL | 252 | (u64{250} << 8) | (u64{lane} << 20)); emit(0x0400000000070000ULL | 252 | (u64{252} << 8) | (u64{1} << 20)); emit(0x5c10000000070000ULL | 252 | (u64{255} << 8) | (u64{252} << 20) | (u64{1} << 48)); - emit(0x5c40000000070000ULL | 251 | (u64{251} << 8) | (u64{252} << 20) | (u64{7} << 48)); - if (maximum) { + // Inactive lanes: 0, or ~0 for the minimum (OR with the inverted lane mask). + emit(0x5c40000000070000ULL | 251 | (u64{251} << 8) | (u64{252} << 20) | (u64{7} << 48) | + (u64{minimum} << 40) | (u64{minimum} << 41)); + if (minimum) { + emit(0x5c20000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | (u64{7} << 39)); + } else if (maximum) { emit(0x5c20000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | (u64{15} << 39)); + } else if (logical) { + emit(0x5c40000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | + (u64{logical} << 41) | (u64{7} << 48)); } else { emit(0x5c10000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20)); } } + if (flip) flip_sign(target); break; } case 0xf8c: { // ATOMS.POPC.INC.32 with no returned value: one increment per active lane. - const u32 stride = insn.Bits(78, 2); - if (dest != 255 || insn.words[1] || (insn.words[2] & ~0xc000U) != 0x0d00003f) + const u32 stride = insn.Bits(78, 2), offset = insn.Bits(40, 24); + if (dest != 255 || (insn.words[1] & 0xff) || (offset & 3) || + (insn.words[2] & ~0xc000U) != 0x0d00003f) return fail(index, "native shared increment mode"); u32 address = insn.Bits(24, 8); if (stride && address != 255) { @@ -896,15 +1349,18 @@ inline LoweredProgram LowerToMaxwell(std::span code) { address = 251; } mov_imm(250, 1); - emit(0xec00000000000000ULL | predicate | 255 | (u64{address} << 8) | (u64{250} << 20)); + emit(0xec00000000000000ULL | predicate | 255 | (u64{address} << 8) | (u64{250} << 20) | + (u64{offset >> 2} << 30)); break; } - case 0x38c: { // ATOMS.ADD.U32, naturally aligned shared byte offset. - const u32 offset = insn.Bits(40, 24); - if (insn.words[2] || (offset & 3)) return fail(index, "native shared atomic mode"); + case 0x38c: { // ATOMS (ADD/MIN/MAX/INC/DEC/AND/OR/XOR/EXCH, U32 or S32), aligned byte offset. + // The operation (bits 87-90) is numbered as in Maxwell ATOMS (52-55); bit 73 is S32. + const u32 offset = insn.Bits(40, 24), operation = insn.Bits(87, 4); + if ((insn.words[2] & ~0x07800200U) || operation > 8 || (offset & 3)) + return fail(index, "native shared atomic mode"); emit(0xec00000000000000ULL | predicate | dest | (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(32, 8)} << 20) | - (u64{offset >> 2} << 30)); + (u64{insn.Bits(73, 1)} << 28) | (u64{offset >> 2} << 30) | (u64{operation} << 52)); break; } case 0x3a8: { // ATOMG.E.ADD.STRONG.GPU U32 at a 64-bit register address plus an offset. @@ -918,6 +1374,23 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{1} << 48)); break; } + case 0x3a9: { // (hookshot) ATOMG.E.CAS.STRONG.GPU PT, Rd, [Ra], Rb, Rc: U32, Rb the comparator, Rc the value. + // Maxwell's ATOM.E.CAS takes them as the pair Rb, Rb+1: copied to R250/R251 unless they are one already. + const u32 address = insn.Bits(24, 8), comparator = insn.Bits(32, 8), value = insn.Bits(64, 8); + const s32 offset = static_cast(insn.Bits(40, 24) << 8) >> 8; + if ((insn.words[2] & ~0xffU) != 0x001ee100 || (address & 1) || address >= 246 || + offset < -0x80000 || offset > 0x7ffff) + return fail(index, "native global compare-and-swap mode"); + u32 pair = comparator; + if ((comparator & 1) || value != comparator + 1 || comparator >= 246) { + emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{comparator} << 20)); + emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{value} << 20)); + pair = 250; + } + emit(0xeef0000000000000ULL | predicate | dest | (u64{address} << 8) | (u64{pair} << 20) | + (u64{static_cast(offset) & 0xfffff} << 28) | (u64{1} << 48)); + break; + } case 0x9a8: { // ATOMG.E.ADD.STRONG.GPU U32 through a uniform pointer plus an offset. const u32 address = uniform_register(insn.Bits(64, 6)); const s32 offset = static_cast(insn.Bits(40, 24) << 8) >> 8; @@ -929,14 +1402,14 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{1} << 48)); break; } - case 0xb97: { // SULD.P.2D/2D_ARRAY.IGN, direct descriptor, channel mask at 72-75. + case 0xb97: { // SULD.P.2D/2D_ARRAY/3D.IGN, direct descriptor, channel mask at 72-75. const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 coordinate = insn.Bits(24, 8); const u32 channels = insn.Bits(72, 4); - const u32 dimension = insn.Bits(61, 3); // Maxwell's codes: 3 = 2D, 4 = 2D_ARRAY - if ((insn.words[2] & ~0xf00U) != 0x001e0000 || channels == 0 || (dimension != 3 && dimension != 4) || + const u32 dimension = insn.Bits(61, 3); // Maxwell's codes: 3 = 2D, 4 = 2D_ARRAY, 5 = 3D + if ((insn.words[2] & ~0xf00U) != 0x001e0000 || channels == 0 || dimension < 3 || dimension > 5 || binding >= 18 || offset > 0x1fff || insn.Bits(59, 2) || dest == 255 || - coordinate > (dimension == 4 ? 252U : 253U)) + coordinate > (dimension >= 4 ? 252U : 253U)) return fail(index, "native formatted surface load mode"); if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native surface descriptor buffer conflict"); @@ -949,31 +1422,66 @@ inline LoweredProgram LowerToMaxwell(std::span code) { const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8); const u32 dimension = insn.Bits(61, 3); - if (insn.words[2] != 0x00100f00 || dimension < 3 || dimension > 5 || + const bool strong = insn.words[2] == 0x0010af00; // .STRONG.GPU: fenced on both sides + if ((insn.words[2] != 0x00100f00 && !strong) || dimension < 3 || dimension > 5 || binding >= 18 || offset > 0x1fff || insn.Bits(59, 2) || data > 252 || coordinate > (dimension >= 4 ? 252U : 253U)) return fail(index, "native formatted surface store mode"); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) - return fail(index, "native surface descriptor buffer conflict"); - result.required_texture_buffer = binding; + // A second descriptor buffer: SUST.B with the handle, loaded from that buffer, in R250. + const bool bindless = !bind_texture_buffer(binding); + if (bindless) { + emit(0x4c98000000070000ULL | 250 | (u64{15} << 39) | (u64{offset} << 20) | (u64{binding} << 34)); + } + if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); // MEMBAR.GL emit(0xeb20000000000000ULL | predicate | data | (u64{coordinate} << 8) | - (u64{15} << 20) | (u64{dimension} << 33) | (u64{offset} << 36) | (u64{1} << 51)); + (u64{15} << 20) | (u64{dimension} << 33) | + (bindless ? u64{250} << 39 : (u64{offset} << 36) | (u64{1} << 51))); + if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); break; } - case 0x59f: { // SURED.D.{1D..3D}.{ADD..XOR}.STRONG.GPU.IGN, U32/S32, direct descriptor. - // Same descriptor fields as SUST.P; size at 73-75 and op at 87-89 use Maxwell's codes. + case 0x59f: { // SURED.D.2D..STRONG.GPU.IGN U32, direct descriptor (title 1's race compute). const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8); - const u32 dimension = insn.Bits(61, 3), size = insn.Bits(73, 3); - if ((insn.words[2] & ~0x03800e00U) != 0x0010e000 || size > 1 || dimension > 5 || - binding >= 18 || offset > 0x1fff || insn.Bits(59, 2) || data == 255 || - coordinate > (dimension >= 4 ? 252U : dimension == 3 || dimension == 2 ? 253U : 254U)) + // (hookshot) bits 87-90: ADD MIN MAX INC DEC AND OR XOR, as Maxwell SURED's 24-26 (OoT's TAAU ORs + // its tile flags) + const u32 reduction = insn.Bits(87, 4); + if ((insn.words[2] & ~(0xfU << 23)) != 0x0010e000 || reduction > 7 || insn.Bits(61, 3) != 3 || binding >= 18 || + offset > 0x1fff || insn.Bits(59, 2) || coordinate > 253) return fail(index, "native surface reduction mode"); if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native surface descriptor buffer conflict"); result.required_texture_buffer = binding; - emit(0xeb40000000000000ULL | predicate | data | (u64{coordinate} << 8) | - (u64{size} << 20) | (u64{insn.Bits(87, 3)} << 24) | (u64{dimension} << 33) | - (u64{offset} << 36) | (u64{1} << 51)); + // Maxwell SURED (as the frontend decodes it): operand 0-7, coordinates 8-15, ADD and + // U32 are zero at 24 and 20, type 33 (3 = 2D), bound offset 36, bound 51. + emit(0xeb40000000000000ULL | predicate | data | (u64{coordinate} << 8) | (u64{reduction} << 24) | + (u64{3} << 33) | (u64{offset} << 36) | (u64{1} << 51)); + break; + } + case 0x243: { // (hookshot) F2IP.U8.F32 Rd, Ra, Rb, Rc: {Rc[15:0], sat_u8(Ra), sat_u8(Rb)} (PTX cvt.pack.sat.u8) + // Bits 78-79 round as I2FP's (RN RM RP RZ, Maxwell F2I's 39-40). OoT packs colours scaled by 255 so. + if ((insn.words[2] & ~(0xffU | (3U << 14))) != 0x400 || (insn.words[1] & ~0xffU)) + return fail(index, "native float-to-packed-integer mode"); + const u32 a = insn.Bits(24, 8), b = insn.Bits(32, 8), c = insn.Bits(64, 8), rounding = insn.Bits(78, 2); + if ((a != 255 && a >= 246) || (b != 255 && b >= 246) || (c != 255 && c >= 246)) + return fail(index, "native float-to-packed-integer registers"); + // max(x, 0) (a NaN gives 0), min(x, 255), then F2I.U32 + const auto byte_of = [&](u32 source, u32 target) { + if (source == 255) { mov_imm(target, 0); return; } + mov_imm(252, 0); + emit(0x5c60000000000000ULL | predicate | target | (u64{source} << 8) | (u64{252} << 20) | (u64{15} << 39)); + mov_imm(252, 0x437f0000); + emit(0x5c60000000000000ULL | predicate | target | (u64{target} << 8) | (u64{252} << 20) | (u64{7} << 39)); + emit(0x5cb0000000000000ULL | predicate | target | (u64{2} << 8) | (u64{2} << 10) | (u64{target} << 20) | + (u64{rounding} << 39)); + }; + byte_of(a, 250); + byte_of(b, 251); + emit(0x3848000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{8} << 20)); // a << 8 + if (c != 255) { + emit(0x5c40000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{251} << 20) | (u64{1} << 41) | + (u64{7} << 48)); + emit(0x3848000000000000ULL | predicate | 251 | (u64{c} << 8) | (u64{16} << 20)); // Rc << 16 + } + emit(base(0x5c40000000000000ULL) | (u64{250} << 8) | (u64{251} << 20) | (u64{1} << 41) | (u64{7} << 48)); break; } case 0x301: // BREV register. @@ -981,21 +1489,36 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(base(0x3828000000000000ULL) | (u64{insn.words[1]} << 8) | (u64{1} << 39) | (u64{1} << 40)); break; - case 0x219: case 0xc19: { // U32 register (or uniform, 0xc19) shifts with a zero word outside the selected result. + case 0x219: case 0xa19: case 0xc19: { // U32 register (or cbuf, 0xa19, or uniform, 0xc19) shifts with a zero word outside the selected result. const u32 low = insn.Bits(24, 8), high = insn.Bits(64, 8); - const u32 shift = opcode == 0xc19 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); + const bool constant = opcode == 0xa19; + // A constant shift amount is loaded into R251 once the form is known. + const u32 shift = opcode == 0xc19 ? uniform_register(insn.Bits(32, 8)) : constant ? 251U : insn.Bits(32, 8); + const bool shift_operand = constant ? valid_cbuf() && !insn.Bits(62, 2) : insn.words[1] <= 255; + const auto load_shift = [&] { if (constant) materialize(2, 0, 251, false, false); }; const bool right = insn.Bits(76, 1) != 0, high_result = insn.Bits(80, 1) != 0; // SHF.R.U32 of the low word with a zero high word: a plain logical right shift. if (right && !high_result && high == 255 && insn.Bits(73, 2) == 3 && shift != ~0U && - insn.words[1] <= 255 && + shift_operand && (insn.words[2] & ~(0x000118ffU | (opcode == 0xc19 ? 0x08000000U : 0U))) == 0x00000600U) { + load_shift(); emit(base(0x5c28000000000000ULL) | (u64{low} << 8) | (u64{shift} << 20)); break; } - if (insn.words[1] > 255 || insn.Bits(73, 2) != 3 || shift == ~0U || + if (shift_operand && shift != ~0U && + !(insn.words[2] & ~(0x00011effU | (opcode == 0xc19 ? 0x08000000U : 0U))) && + (insn.Bits(73, 2) != 3 || (insn.words[2] & 0x800U) || + (right ? (!high_result || low != 255) : (high_result || high != 255)))) { + load_shift(); // (hookshot) any type, both words, wrap: the general funnel shift + funnel(static_cast(dest), low, high, true, shift, insn.Bits(73, 2), right, high_result, + insn.Bits(75, 1) != 0); + break; + } + if (!shift_operand || insn.Bits(73, 2) != 3 || shift == ~0U || (insn.words[2] & ~(0x000118ffU | (opcode == 0xc19 ? 0x08000000U : 0U))) != 0x00000600U || (right ? (!high_result || low != 255) : (high_result || high != 255))) return fail(index, "native register shift mode"); + load_shift(); emit(base(right ? 0x5cf8000000000000ULL : 0x5bf8000000000000ULL) | (u64{right ? high : 255U} << 8) | (u64{shift} << 20) | (u64{right ? 255U : low} << 39) | (u64{insn.Bits(75, 1)} << 50)); @@ -1034,8 +1557,8 @@ inline LoweredProgram LowerToMaxwell(std::span code) { if (insn.words[1] || insn.words[2]) return fail(index, "native error barrier mode"); emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); break; - case 0xb1d: // BAR.SYNC 0, full workgroup. - if (insn.words[1] || insn.words[2] || native_predicate != 7) + case 0xb1d: // BAR.SYNC 0 (bit 80: .DEFER_BLOCKING), full workgroup. + if (insn.words[1] || (insn.words[2] & ~0x10000U) || native_predicate != 7) return fail(index, "native workgroup barrier mode"); emit(0xf0a8000000000000ULL | predicate | (u64{1} << 43) | (u64{1} << 44) | (u64{1} << 39) | (u64{7} << 39)); @@ -1097,11 +1620,16 @@ inline LoweredProgram LowerToMaxwell(std::span code) { // minutes to compile, so it stays opt-in (the QMD still completes when rejected). // EDEN_NX2_TENSOR=stub keeps the rest of the program but makes every HMMA D = C. static const char* const mode = std::getenv("EDEN_NX2_TENSOR"); - if (!mode) return fail(index, "native matrix multiply (set EDEN_NX2_TENSOR to emulate)"); const u32 a = insn.Bits(24, 8), b = insn.Bits(32, 8), c = insn.Bits(64, 8); if ((insn.words[1] & ~0xffU) || (insn.words[2] & ~0xffU) != 0x800 || dest + 2 > 246 || a + 4 > 246 || b + 2 > 246 || (c != 255 && c + 2 > 246)) return fail(index, "native matrix multiply mode"); + // HMMA_nx2: dest 0-7, A 8-15, B 20-27, C 39-46 (255: zero), the predicate as usual + if (native_matrix_multiply && !(mode && *mode)) { + emit(0xefc8000000000000ULL | predicate | dest | (u64{a} << 8) | (u64{b} << 20) | (u64{c} << 39)); + break; + } + if (!mode) return fail(index, "native matrix multiply (set EDEN_NX2_TENSOR to emulate)"); if (std::string_view{mode} == "stub") { for (u32 row = 0; row < 2; ++row) { if (c == 255) mov_imm(static_cast(dest) + row, 0); @@ -1210,10 +1738,21 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } break; } - case 0x919: case 0x9c3: { // S2R / S2UR thread and workgroup identities. + case 0x919: case 0x9c3: { // S2R / S2UR: special registers the Maxwell S2R handles. + // Same numbering as Maxwell: lane id (0), invocation id/info, Y direction, thread kill + // (17-19, 29), W scale factors (30-31, Kirby Air Riders), thread and workgroup ids, + // lane masks (56-60). const u32 special = insn.Bits(72, 8); + if (special == 132 && !insn.words[1] && insn.words[2] == (special << 8)) { + // SR_VARIABLE_RATE: without variable-rate shading every fragment is 1x1, its width + // and height in bytes 1 and 2 (Nintendo Switch Sports Resort multiplies them). + const u32 target = opcode == 0x9c3 ? uniform_register(static_cast(dest)) : static_cast(dest); + if (target == ~0U) return fail(index, "native uniform special destination"); + mov_imm(target, 0x00010100); + break; + } if (insn.words[1] || insn.words[2] != (special << 8) || - (special != 0 && special != 33 && special != 34 && special != 35 && + (special != 0 && (special < 17 || special > 19) && (special < 29 || special > 35) && special != 37 && special != 38 && special != 39 && special != 56 && special != 57 && special != 58 && special != 59 && special != 60)) return fail(index, "native special register"); @@ -1225,43 +1764,87 @@ inline LoweredProgram LowerToMaxwell(std::span code) { case 0x28c: case 0x88c: { // UISETP, signed (bit 73) or unsigned, with UPT accumulation. const u32 output = insn.Bits(81, 3); const u32 a = uniform_register(insn.Bits(24, 8)), b = opcode == 0x88c ? 252U : uniform_register(insn.Bits(32, 8)); - if (output > 6 || insn.Bits(84, 3) != 7 || insn.Bits(87, 4) != 7 || - (insn.Bits(72, 4) & ~2U) || insn.Bits(12, 4) != 7 || a == ~0U || b == ~0U) + // (hookshot) a UPn guard: R239 kept in R249 first; where the guard (read from that copy: the compare may + // write the very UPn that guards it) is false, R239 gets the copy back + const u32 guard = insn.Bits(12, 4); + const bool guarded_compare = guard != 7 && (guard & 7) != 7; + // (hookshot) an accumulator UPn (bits 87-90) and boolean op (74-75): loaded from R239 into its own slot + // P(6-n) (the result's, when it is the output) and given to ISETP as its combining input + const u32 accumulator = insn.Bits(87, 4), bop = insn.Bits(74, 2); + const u32 acc_slot = (accumulator & 7) == 7 ? 7U : 6 - (accumulator & 7); + if (output > 6 || insn.Bits(84, 3) != 7 || bop > 2 || + (insn.Bits(72, 4) & ~0xeU) || (guard != 7 && !guarded_compare) || a == ~0U || b == ~0U) return fail(index, "native uniform comparison mode"); - const u32 temporary = 6 - output, mask = 1U << temporary; - if (opcode == 0x88c) mov_imm(252, insn.words[1]); + const u32 temporary = 6 - output, mask = (1U << temporary) | (acc_slot < 7 ? 1U << acc_slot : 0U); + if (guarded_compare) emit(0x5c98000000070000ULL | 249 | (u64{239} << 20) | (u64{15} << 39)); + if (opcode == 0x88c) emit(0x0100000000000000ULL | (u64{7} << 16) | 252 | (u64{15} << 12) | (u64{insn.words[1]} << 20)); // (hookshot, from Lichii: unguarded, its guard is a uniform predicate) emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{mask} << 20)); + if (acc_slot < 7) emit(0x38f0000000070000ULL | (u64{239} << 8) | (u64{1U << acc_slot} << 20)); emit(0x5b60000000070000ULL | 7 | (u64{temporary} << 3) | - (u64{a} << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{insn.Bits(73, 1)} << 48) | - (u64{insn.Bits(76, 3)} << 49)); - emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{mask} << 20)); + (u64{a} << 8) | (u64{b} << 20) | (u64{acc_slot} << 39) | (u64{(accumulator >> 3) & 1} << 42) | + (u64{bop} << 45) | (u64{insn.Bits(73, 1)} << 48) | (u64{insn.Bits(76, 3)} << 49)); + emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{1U << temporary} << 20)); emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{mask} << 20)); + if (guarded_compare) { + const u32 slot = 6 - (guard & 7), guard_bit = 1U << slot; + emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{guard_bit} << 20)); + emit(0x38f0000000070000ULL | (u64{249} << 8) | (u64{guard_bit} << 20)); + emit(0x5ca0000000070000ULL | 239 | (u64{239} << 8) | (u64{249} << 20) | (u64{slot} << 39) | + (u64{(guard >> 3) & 1} << 42)); + emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{guard_bit} << 20)); + } break; } case 0x287: case 0x887: { // USEL, UP0/UP1; preserve ordinary predicates. const u32 condition = insn.Bits(87, 3); const u32 target = uniform_register(static_cast(dest)); const u32 a = uniform_register(insn.Bits(24, 8)), b = opcode == 0x887 ? 252U : uniform_register(insn.Bits(32, 8)); - if (condition > 6 || target == ~0U || a == ~0U || b == ~0U || - insn.Bits(12, 4) != 7 || (insn.words[2] & ~0x07800000U) != 0x08000000U) + // (hookshot) a UPn guard (bits 12-15): the select into R249, then kept only where the guard holds + const u32 guard = insn.Bits(12, 4); + const bool guarded_select = guard != 7 && (guard & 7) != 7; + if (condition > 6 || target == ~0U || a == ~0U || b == ~0U || (guard != 7 && !guarded_select) || + (insn.words[2] & ~0x07800000U) != 0x08000000U) return fail(index, "native uniform select mode"); - if (opcode == 0x887 && index > 0 && condition == 0 && insn.words[1] <= 0x7ffff && - code[index - 1].Bits(0, 12) == 0x88c && - code[index - 1].Bits(24, 8) == insn.Bits(24, 8) && - code[index - 1].words[1] == insn.words[1] && code[index - 1].words[2] == 0x0bf01070 && + // The clamp's UISETP may sit a few instructions back, with only instructions between + // that write no uniform state and do not branch (Nintendo Switch Sports' jump tables), + // and no branch seen so far landing between them. + u32 compare = index; + for (u32 back = 1; back <= 8 && back <= index; ++back) { + const u32 earlier = code[index - back].Bits(0, 12), datapath = earlier & 0x1ff; + if (earlier == 0x88c) { + compare = index - back; + break; + } + if ((datapath >= 0x080 && datapath <= 0x0bf) || (datapath >= 0x1c2 && datapath <= 0x1c4) || + (earlier >= 0x940 && earlier <= 0x95f)) + break; + } + const bool target_between = std::any_of(relocations.begin(), relocations.end(), + [&](const Relocation& relocation) { return relocation.target > compare && relocation.target <= index; }); + if (!guarded_select && opcode == 0x887 && compare < index && !target_between && condition == 0 && insn.words[1] <= 0x7ffff && + code[compare].Bits(12, 4) == 7 && code[compare].Bits(24, 8) == insn.Bits(24, 8) && + code[compare].words[1] == insn.words[1] && code[compare].words[2] == 0x0bf01070 && insn.words[2] == 0x08000000) { // Preserve the unsigned clamp idiom for indirect-table analysis. emit(0x3820000000070000ULL | target | (u64{a} << 8) | (u64{insn.words[1]} << 20) | (u64{7} << 39)); break; } - if (opcode == 0x887) mov_imm(252, insn.words[1]); + if (opcode == 0x887) emit(0x0100000000000000ULL | (u64{7} << 16) | 252 | (u64{15} << 12) | (u64{insn.words[1]} << 20)); // (hookshot, from Lichii) const u32 temporary = 6 - condition, mask = 1U << temporary; emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{mask} << 20)); emit(0x38f0000000070000ULL | (u64{239} << 8) | (u64{mask} << 20)); - emit(0x5ca0000000070000ULL | target | (u64{a} << 8) | (u64{b} << 20) | + emit(0x5ca0000000070000ULL | (guarded_select ? 249U : target) | (u64{a} << 8) | (u64{b} << 20) | (u64{temporary} << 39) | (u64{insn.Bits(90, 1)} << 42)); emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{mask} << 20)); + if (guarded_select) { + const u32 slot = 6 - (guard & 7), guard_bit = 1U << slot; + emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{guard_bit} << 20)); + emit(0x38f0000000070000ULL | (u64{239} << 8) | (u64{guard_bit} << 20)); + emit(0x5ca0000000070000ULL | target | (u64{249} << 8) | (u64{target} << 20) | (u64{slot} << 39) | + (u64{(guard >> 3) & 1} << 42)); + emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{guard_bit} << 20)); + } break; } case 0x381: case 0x981: { // LDG.E of 8 to 128 bits, register or uniform 64-bit address. @@ -1279,10 +1862,17 @@ inline LoweredProgram LowerToMaxwell(std::span code) { // The uniform base sits where a register source would (bits 32-37). const u32 uniform_address = insn.Bits(38, 2) ? ~0U : uniform_register(insn.Bits(32, 6)); - if ((insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U | 0x2000U)) != 0x081e8100 || + // desc[URn][Ra.64]: the uniform pair is a memory (cache policy) descriptor and + // Ra the whole address. + const bool descriptor_address = (insn.words[2] & ~(0x3fU | 0xe00U)) == 0x0c1e1100; + if ((!descriptor_address && + (insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U | 0x2000U)) != 0x081e8100) || uniform_address == ~0U || (uniform_address & 1)) return fail(index, "native global load uniform addressing mode"); - if (address == 255) { + if (descriptor_address) { + if ((address & 1) || address >= 246) + return fail(index, "native global load register address pair"); + } else if (address == 255) { address = uniform_address; } else { if (insn.Bits(90, 1) && ((address & 1) || address >= 246)) @@ -1315,10 +1905,16 @@ inline LoweredProgram LowerToMaxwell(std::span code) { bool extended = insn.Bits(72, 1) != 0; if (opcode == 0x986) { const u32 uniform_address = uniform_register(insn.Bits(64, 6)); - if ((insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U)) != 0x0810a100 || + // desc[URn][Ra.64]: the uniform pair is a memory (cache policy) descriptor and + // Ra the whole address. + const bool descriptor_address = (insn.words[2] & ~(0x3fU | 0xe00U)) == 0x0c101100; + if ((!descriptor_address && (insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U)) != 0x0810a100) || uniform_address == ~0U || (uniform_address & 1) || !extended) return fail(index, "native global store uniform addressing mode"); - if (address == 255) address = uniform_address; + if (descriptor_address) { + if ((address & 1) || address >= 246) + return fail(index, "native global store register address pair"); + } else if (address == 255) address = uniform_address; else { if (insn.Bits(90, 1) && ((address & 1) || address >= 254)) return fail(index, "native global store register address pair"); @@ -1341,22 +1937,43 @@ inline LoweredProgram LowerToMaxwell(std::span code) { return fail(index, "native fragment discard mode"); emit(0xe33000000000000fULL | predicate); break; + case 0x311: { // F2I.FTZ.U64.TRUNC, register source, destination pair + if (insn.words[2] != 0x0021d800 || (insn.words[1] & ~0xffU) || insn.Bits(24, 8) || + (dest & 1) || dest >= 246) + return fail(index, "native unsigned 64-bit float conversion mode"); + emit(base(0x5cb0000000000000ULL) | (u64{3} << 8) | (u64{2} << 10) | + (u64{insn.Bits(32, 8)} << 20) | (u64{3} << 39) | (u64{1} << 44)); + break; + } + case 0x312: { // I2F.F32.(U|S)64, register pair source; sign (74) and rounding (78-79) as Maxwell's + if ((insn.words[2] & ~0xc400U) != 0x00301000 || (insn.words[1] & ~0xffU) || insn.Bits(24, 8) || + (insn.Bits(32, 8) & 1) || insn.Bits(32, 8) >= 254) + return fail(index, "native 64-bit integer-to-float mode"); + emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{3} << 10) | (u64{insn.Bits(74, 1)} << 13) | + (u64{insn.Bits(32, 8)} << 20) | (u64{insn.Bits(78, 2)} << 39)); + break; + } case 0x305: case 0xb05: case 0xd05: { // F2I NTZ, register/constant-buffer/uniform source const u32 mode = insn.words[2] & ~(opcode == 0xd05 ? 0x08000000U : 0U); - const bool half = opcode == 0x305 && mode == 0x0010f000; + // F16 source: bit 72 signs the result as in the other forms, bits 60-61 pick H0/H1. + const bool half = opcode == 0x305 && (mode & ~0x100U) == 0x0010f000; const bool small = mode == 0x00212800; const bool floor = (mode & ~0x10100U) == 0x00207000; const bool nearest = (mode & ~0x10100U) == 0x00203000; - if ((!half && !small && !floor && !nearest && (mode & ~0x100U) != 0x0021f000) || - (opcode != 0xb05 && (insn.words[1] & 0xffffff00U)) || + const bool ceil = (mode & ~0x10100U) == 0x0020b000; // (hookshot) round up: Maxwell's mode 2 + // (hookshot) a negated or absolute register source (bits 63, 62), made first + const bool source_mod = opcode == 0x305 && !half && (insn.words[1] & 0x3fffff00U) == 0; + if ((!half && !small && !floor && !nearest && !ceil && (mode & ~0x10100U) != 0x0020f000) || + (opcode != 0xb05 && !source_mod && (insn.words[1] & (half ? 0xefffff00U : 0xffffff00U))) || (opcode == 0xd05 && uniform_register(insn.Bits(32, 8)) == ~0U)) { return fail(index, "native float-to-integer conversion mode"); } const bool is_signed = insn.Bits(72, 1); u32 source = opcode == 0xb05 ? materialize(2, 0, 252, false, false) : opcode == 0xd05 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); + if (source_mod && insn.Bits(62, 2)) source = materialize(0, insn.Bits(32, 8), 249, insn.Bits(62, 1), insn.Bits(63, 1)); if (half) { - extract_half({source, 2, false, false}, 0, 252); + extract_half({source, insn.Bits(60, 1) ? 3U : 2U, false, false}, 0, 252); source = 252; } if (!is_signed) { @@ -1378,26 +1995,49 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } emit(base(0x5cb0000000000000ULL) | (u64{small ? 1U : 2U} << 8) | (u64{2} << 10) | (u64{is_signed} << 12) | (u64{source} << 20) | - (u64{small || nearest ? 0U : floor ? 1U : 3U} << 39) | (u64{insn.Bits(80, 1)} << 44)); + (u64{small || nearest ? 0U : floor ? 1U : ceil ? 2U : 3U} << 39) | (u64{insn.Bits(80, 1)} << 44)); if (!is_signed && !small) { emit(base(0x5c40000000000000ULL) | (dest << 8) | (u64{251} << 20) | (u64{1} << 41) | (u64{7} << 48)); } break; } - case 0xb6f: { // TXQ texture dimensions, two packed scalar outputs. - if (insn.Bits(59, 5) || insn.Bits(64, 8) != 255 || insn.Bits(72, 4) == 0 || insn.Bits(72, 4) > 3 || - insn.Bits(76, 14) || insn.Bits(91, 1) || insn.Bits(54, 5) != 2 || - insn.Bits(40, 14) > 0x1fff || (insn.Bits(72, 4) == 3 && (dest & 1)) || dest >= 254) + case 0xb6f: case 0x370: { // TXQ texture dimensions (0x370: TXQ.B). + // TXQ.B takes the handle in Ra and the level in Ra+1, as Maxwell's. Up to two outputs + // go to Rd, Rd+1 directly; more, or a second destination (bits 64-71) for the third and + // fourth, come out packed in scratch and are copied as for TEX. + const bool bindless = opcode == 0x370; + const u32 binding = insn.Bits(54, 5); // descriptor buffer, as for TLD + const u32 mask = insn.Bits(72, 4), components = static_cast(std::popcount(mask)); + const u32 second_dest = insn.Bits(64, 8); + const bool direct = second_dest == 255; + if (insn.Bits(59, 5) != (bindless ? 1U : 0U) || mask == 0 || (direct && components > 2) || + insn.Bits(76, 14) || insn.Bits(91, 1) || (!bindless && binding >= 18) || + (bindless && (insn.Bits(32, 27) || insn.Bits(24, 8) >= 245)) || + (!direct && (second_dest >= 245 || dest >= 245)) || + insn.Bits(40, 14) > 0x1fff || (direct && components == 2 && (dest & 1)) || dest >= 254) return fail(index, "native texture dimension query mode"); - result.required_texture_buffer = 2; - emit(0xdf48000000000000ULL | predicate | dest | (u64{insn.Bits(24, 8)} << 8) | - (u64{1} << 22) | (u64{insn.Bits(72, 4)} << 31) | (u64{insn.Bits(40, 14)} << 36) | - (u64{insn.Bits(90, 1)} << 49)); + if (!bindless) { + if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + return fail(index, "native texture descriptor buffer conflict"); + result.required_texture_buffer = binding; + } + emit((bindless ? 0xdf50000000000000ULL : 0xdf48000000000000ULL) | predicate | (direct ? dest : 250) | + (u64{insn.Bits(24, 8)} << 8) | (u64{1} << 22) | (u64{mask} << 31) | + (u64{bindless ? 0U : insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49)); + for (u32 i = 0; !direct && i < components; ++i) { + const u32 target = i < 2 ? (dest == 255 ? 255U : static_cast(dest) + i) : second_dest + (i - 2); + if (target != 255) + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{250 + i} << 20)); + } break; } - case 0xb66: { // TLD.SCR.LZ, direct texel fetch + case 0xb66: { // TLD.SCR, direct texel fetch at level zero (.LZ) or a given level (.LL) const u32 mask = insn.Bits(72, 4); + const bool level = insn.Bits(87, 3) == 3; // .LL: the level follows the coordinates + // Bit 76: a texel offset after the coordinates and level. 2D only, as Maxwell TLDS + // 2D.LZ.AOFFI (x, y in Ra, Ra+1, offsets in Rb) or 2D.LL.AOFFI (level, offsets in Rb, Rb+1). + const bool offsets = insn.Bits(76, 1) != 0; const u32 dimension = insn.Bits(61, 3); const bool array = dimension == 5; const bool volume = dimension == 2; @@ -1413,8 +2053,12 @@ inline LoweredProgram LowerToMaxwell(std::span code) { for (u32 i = 0; i < rg_masks.size(); ++i) if (rg_masks[i] == mask) swizzle = i; } if (!insn.Bits(60, 1) || (dimension != 0 && dimension != 1 && !array && !volume) || - swizzle == ~0U || insn.Bits(76, 5) || - insn.Bits(81, 3) != 7 || insn.Bits(87, 3) != 1 || + swizzle == ~0U || (insn.Bits(76, 5) & ~1U) || + (offsets && (dimension != 1 || (source_a & 1) || source_a >= 254 || source_b == 255 || + (level && ((source_b & 1) || source_b >= 254)))) || + insn.Bits(81, 3) != 7 || (insn.Bits(87, 3) != 1 && !level) || (level && volume) || + (level && dimension == 1 && ((source_a & 1) || source_a >= 254)) || + (level && array && ((source_b & 1) || source_b >= 254)) || insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 || dest == 255 || (mask & (mask - 1) && ((dest & 1) || dest >= 254)) || @@ -1424,9 +2068,46 @@ inline LoweredProgram LowerToMaxwell(std::span code) { return fail(index, "native texel fetch mode or descriptor binding"); } const u32 binding = insn.Bits(54, 5); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) - return fail(index, "native texture descriptor buffer conflict"); - result.required_texture_buffer = binding; + if (!bind_texture_buffer(binding)) { + if (level || offsets) + return fail(index, "native texture descriptor buffer conflict"); + // A second descriptor buffer: TLD.B (LZ) with the coordinates in R250.. (array + // index or Z after Ra) and the handle, loaded from that buffer, in R253. + const auto copy = [&](u32 target, u32 source) { + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); + }; + copy(250, source_a); + if (array || volume) copy(251, source_a + 1); + if (dimension != 0) copy(array || volume ? 252 : 251, source_b); + emit(0x4c98000000070000ULL | 253 | (u64{15} << 39) | (u64{insn.Bits(40, 14)} << 20) | + (u64{binding} << 34)); + emit(0xdd00000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{253} << 20) | + (u64{array ? 3U : volume ? 4U : dimension == 0 ? 0U : 2U} << 28) | (u64{mask} << 31) | + (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); + const u32 components = static_cast(std::popcount(mask)); + for (u32 i = 0; i < components; ++i) + copy(i < 2 || !split ? static_cast(dest) + i : second_dest + (i - 2), 250 + i); + break; + } + if (array && level) { + // TLDS has no array form with a level: TLD takes (layer, x, y) from Ra and the level + // from Rb (layer, x in Ra, Ra+1; y, level in Rb, Rb+1 here). Results come out packed, + // then go where the Ampere destinations (Rd, Rd+1, then the second pair) take them. + for (u32 i = 0; i < 2; ++i) { + emit(0x5c98000000000000ULL | predicate | (250 + i) | (u64{15} << 39) | (u64{source_a + i} << 20)); + emit(0x5c98000000000000ULL | predicate | (252 + i) | (u64{15} << 39) | (u64{source_b + i} << 20)); + } + emit(0xdc00000000000000ULL | predicate | 246 | (u64{250} << 8) | (u64{253} << 20) | + (u64{3} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | + (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{1} << 55)); + const u32 components = static_cast(std::popcount(mask)); + for (u32 i = 0; i < components; ++i) { + const u32 target = !split || i < 2 ? static_cast(dest) + i : second_dest + (i - 2); + if (target != 255 && (split || i < 2)) + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{246 + i} << 20)); + } + break; + } if (array) { emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a + 1} << 20)); emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{source_b} << 20)); @@ -1435,30 +2116,79 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(0xda00000000000000ULL | predicate | dest | (u64{source_a} << 8) | (u64{source_b} << 20) | (u64{insn.Bits(64, 8)} << 28) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | - (u64{swizzle} << 50) | (u64{array ? 8U : volume ? 7U : dimension == 0 ? 0U : 2U} << 53)); + (u64{swizzle} << 50) | + (u64{offsets ? (level ? 12U : 4U) + : array ? 8U : volume ? 7U : dimension == 0 ? (level ? 1U : 0U) : (level ? 5U : 2U)} << 53)); + break; + } + case 0x367: { // TLD.B at level zero (.LZ), the handle in a register. + // The values are the coordinates, then the handle, split over Ra and Rb as for TEX.B + // (ARRAY_2D: layer, x | y, handle); Maxwell TLD.B takes the handle first in Rb. + constexpr std::array coordinate_count{1, 2, 3, 0, 0, 3, 0, 0}; + constexpr std::array maxwell_type{0, 2, 4, 0, 0, 3, 0, 0}; + const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); + const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); + const u32 dimension = insn.Bits(61, 3), coordinates = coordinate_count[dimension]; + const u32 count = coordinates + 1, in_a = (count + 1) / 2; + if (coordinates == 0 || !insn.Bits(59, 1) || !insn.Bits(60, 1) || insn.Bits(40, 19) || + insn.Bits(76, 5) || insn.Bits(81, 3) != 7 || insn.Bits(87, 3) != 1 || insn.Bits(91, 1) || + mask == 0 || source_a == 255 || source_a + in_a > 246 || source_b == 255 || + source_b + (count - in_a) > 246) + return fail(index, "native bindless texel fetch mode"); + const auto value = [&](u32 i) { return i < in_a ? source_a + i : source_b + (i - in_a); }; + const auto copy = [&](u32 target, u32 source) { + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); + }; + for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i)); + copy(246, value(coordinates)); + emit(0xdd00000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | + (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{insn.Bits(90, 1)} << 49) | + (u64{7} << 51)); + const u32 components = static_cast(std::popcount(mask)); + const bool split = second_dest != 255; + for (u32 i = 0; i < components; ++i) { + u32 target = 255; + if (!split || i < 2) target = dest == 255 ? 255U : static_cast(dest) + i; + else target = second_dest + (i - 2); + if (target != 255) copy(target, 250 + i); + } break; } case 0x91a: // DEPBAR.LE SB5, 1: scoreboard wait, represented by IR data dependencies. - if (insn.words[0] != 0x791a || (insn.words[1] & ~0xfc0U) != 0xd000 || insn.words[2]) { + // (hookshot) any scoreboard and count (OoT's mesh programs wait on SB0 at 14 or 30): a wait is no work here + if (insn.words[0] != 0x791a || (insn.words[1] & ~0xffffU) || insn.words[2]) { return fail(index, "native dependency barrier mode"); } break; - case 0xb69: { // TMML (LOD query), 2D. Results are 8.8 fixed point, as on Maxwell. + case 0xb69: case 0xf69: case 0x36a: { // TMML (LOD query), 1D/2D/3D/CUBE. Results are 8.8 fixed point, as on Maxwell. + // 0xf69: the handle in a uniform register (bit 91); 0x36a, TMML.B: the handle in Rb and + // every coordinate in Ra. Both become Maxwell TMML.B with the handle in Rb. + const bool bindless = opcode != 0xb69; + const u32 handle = opcode == 0xf69 ? uniform_register(insn.Bits(40, 6)) + : opcode == 0x36a ? insn.Bits(32, 8) : 255; const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); - const u32 binding = insn.Bits(54, 5); - if (insn.Bits(61, 3) != 1 || insn.Bits(76, 4) || insn.Bits(80, 10) || insn.Bits(59, 1) || - insn.Bits(40, 14) > 0x1fff || binding >= 18 || mask == 0 || source_a >= 246 || - (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)) + const u32 binding = insn.Bits(54, 5), dimension = insn.Bits(61, 3); + // Coordinates: all from Ra when Rb is RZ, else split as for TEX (2D: Ra, Rb). + constexpr std::array coordinate_count{1, 2, 3, 3}; + const u32 coordinates = dimension < 4 ? coordinate_count[dimension] : 0; + const u32 in_a = source_b == 255 || opcode == 0x36a ? coordinates : (coordinates + 1) / 2; + if (coordinates == 0 || insn.Bits(76, 4) || insn.Bits(80, 10) || mask == 0 || + insn.Bits(59, 1) != (opcode == 0x36a ? 1U : 0U) || source_a + in_a > 246 || + (in_a < coordinates && source_b + (coordinates - in_a) > 246) || + (opcode == 0xf69 && (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1))) || + (opcode == 0x36a && (handle >= 246 || insn.Bits(40, 19) || insn.Bits(91, 1))) || + (!bindless && (insn.Bits(40, 14) > 0x1fff || binding >= 18 || + (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)))) return fail(index, "native texture LOD query mode"); - result.required_texture_buffer = binding; - // Coordinates: (Ra, Ra+1) when Rb is RZ, else (Ra, Rb). - emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20)); - emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | - (u64{source_b == 255 ? source_a + 1 : source_b} << 20)); - emit(0xdf58000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | - (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | - (u64{insn.Bits(90, 1)} << 49)); + if (!bindless) result.required_texture_buffer = binding; + for (u32 i = 0; i < coordinates; ++i) + emit(0x5c98000000000000ULL | predicate | (250 + i) | (u64{15} << 39) | + (u64{i < in_a ? source_a + i : source_b + (i - in_a)} << 20)); + if (bindless) emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); + emit((bindless ? 0xdf60000000000000ULL : 0xdf58000000000000ULL) | predicate | 250 | (u64{250} << 8) | + (u64{bindless ? 246U : 255U} << 20) | (u64{dimension * 2} << 28) | (u64{mask} << 31) | + (u64{bindless ? 0U : insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49)); const u32 components = static_cast(std::popcount(mask)); const bool split = second_dest != 255; for (u32 i = 0; i < components; ++i) { @@ -1470,22 +2200,63 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } break; } - case 0xb63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88. + case 0xb63: case 0xf63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88. + // 0xf63: the handle in a uniform register (bit 91), Maxwell TLD4.B with the handle in Rb. + const bool bindless = opcode == 0xf63; + const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255; const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); const u32 binding = insn.Bits(54, 5); - if (insn.Bits(61, 3) != 1 || insn.Bits(76, 4) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - insn.Bits(40, 14) > 0x1fff || binding >= 18 || insn.Bits(89, 1) || mask == 0 || - (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)) + // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offsets in Rb, as + // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb. + // (hookshot) bits 76-77 the offset mode: 1 one offset (AOFFI), 2 four per-pixel offsets (PTP, + // textureGatherOffsets) in Rb, Rb+1; Maxwell TLD4 takes them the same way (mode 2 at bits 54-55, R246-R247) + const bool ptp = insn.Bits(76, 2) == 2; + const bool offsets = insn.Bits(76, 1) != 0 || ptp; + // (hookshot) 2D arrays (bits 61-63 = 5) without offsets too: (layer, x) in Ra, Ra+1 and y in Rb, as + // TLD reads them; Maxwell TLD4's ARRAY_2D (3 at bits 28-30) takes layer, x, y from R250..R252. + const bool gather_array = insn.Bits(61, 3) == 5; + // (hookshot) bit 78: depth compare (TLD4.DC, a shadow gather): x, y in Ra, Ra+1 and the reference in Rb, + // as an offset would be; Maxwell TLD4.DC (bit 50) reads it from the meta register + const bool depth_compare = insn.Bits(78, 2) == 1; + if (depth_compare && (offsets || gather_array || bindless || source_a >= 245 || source_b == 255 || + insn.Bits(87, 2))) return fail(index, "native texture gather mode"); - result.required_texture_buffer = binding; - // Two coordinates: x in Ra, y in Rb (the coordinate list splits evenly across them). + if ((insn.Bits(61, 3) != 1 && !(gather_array && !offsets && source_a < 245)) || (insn.Bits(78, 2) && !depth_compare) || + (insn.Bits(77, 1) && !ptp) || (ptp && (bindless || source_b >= 245)) || + insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || + (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) || + insn.Bits(89, 1) || mask == 0 || + (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) + : (insn.Bits(40, 14) > 0x1fff || binding >= 18 || + (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)))) + return fail(index, "native texture gather mode"); + if (!bindless) result.required_texture_buffer = binding; + // Two coordinates: x in Ra, y in Rb (the coordinate list splits evenly across them); + // with an offset, x and y in Ra, Ra+1 and the offset in Rb. + const u32 y = !offsets && !depth_compare ? source_b : source_a == 255 ? 255U : source_a + 1; + const u64 gather_type = gather_array ? 3U : 2U; emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20)); - emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | - (u64{source_b == 255 ? 255U : source_b} << 20)); - emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | - (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | - (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{insn.Bits(87, 2)} << 56)); + if (gather_array) { + emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{source_a + 1} << 20)); + emit(0x5c98000000000000ULL | predicate | 252 | (u64{15} << 39) | (u64{source_b} << 20)); + } else + emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20)); + if (offsets || depth_compare) { + emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{source_b} << 20)); + if (ptp) emit(0x5c98000000000000ULL | predicate | 247 | (u64{15} << 39) | (u64{source_b + 1} << 20)); + } + if (bindless) { + emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); + emit(0xdec0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | + (gather_type << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | + (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); + } else { + emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | + (u64{offsets || depth_compare ? 246U : 255U} << 20) | (gather_type << 28) | (u64{mask} << 31) | + (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | (u64{7} << 51) | + (u64{ptp ? 2U : offsets ? 1U : 0U} << 54) | (u64{insn.Bits(87, 2)} << 56)); + } const u32 components = static_cast(std::popcount(mask)); const bool split = second_dest != 255; for (u32 i = 0; i < components; ++i) { @@ -1497,7 +2268,15 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } break; } - case 0xb60: { // TEX, scalar 2D implicit-LOD sample + case 0xb60: case 0xf60: case 0x361: { // TEX; 0xf60: handle in a uniform register; 0x361: TEX.B + // Bindless forms become Maxwell TEX.B, the handle first among the meta registers (the + // texture pass tracks it back to its constant buffer words). 0x361 lists the handle + // right after the coordinates, then the LOD and the offsets (see general_tex). + const bool bindless = opcode != 0xb60; + const u32 handle = opcode == 0xf60 ? uniform_register(insn.Bits(40, 6)) : 255; + if (bindless && ((opcode == 0xf60 && (handle == ~0U || handle >= 246 || insn.Bits(46, 13))) || + (opcode == 0x361 && (insn.Bits(40, 19) || insn.Bits(87, 3) > 3)))) + return fail(index, "native bindless texture mode"); const u32 mask = insn.Bits(72, 4); const u32 second_dest = insn.Bits(64, 8); constexpr std::array rg_masks{1, 2, 4, 8, 3, 9, 10, 12}; @@ -1507,12 +2286,12 @@ inline LoweredProgram LowerToMaxwell(std::span code) { const bool depth_compare = insn.Bits(78, 1); const u32 dimension = insn.Bits(61, 3), lod = insn.Bits(87, 3); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); - if (dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) { + if (!bindless && dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) { if (!insn.Bits(60, 1) || insn.Bits(76, 2) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) != 2 || (source_a & 1) || (source_b & 1) || source_a >= 248 || source_b >= 248 || dest >= 248) return fail(index, "native array depth comparison sample mode"); - result.required_texture_buffer = 2; + if (!bind_texture_buffer(2)) return fail(index, "native texture descriptor buffer conflict"); for (u32 i = 0; i < 3; ++i) { const u32 source = i < 2 ? source_a + i : source_b; emit(0x5c98000000000000ULL | predicate | (250 + i) | @@ -1532,14 +2311,23 @@ inline LoweredProgram LowerToMaxwell(std::span code) { constexpr std::array coordinate_count{1, 2, 3, 3, 2, 3, 0, 4}; constexpr std::array maxwell_type{0, 2, 4, 6, 1, 3, 0, 7}; const bool offsets = insn.Bits(76, 1) != 0; - if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 || (lod > 3 && lod != 6 && lod != 7) || - (depth_compare && mask != 1) || mask == 0) + // Bit 59 is .B, set in 0x361's encoding. + if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || + insn.Bits(59, 1) != (opcode == 0x361 ? 1U : 0U) || + (!bindless && (insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18)) || + (lod > 3 && lod != 6 && lod != 7) || (depth_compare && mask != 1) || mask == 0) return false; const u32 coordinates = coordinate_count[dimension]; const bool has_lod = lod == 2 || lod == 3 || lod == 6 || lod == 7; - const u32 count = coordinates + (has_lod ? 1 : 0) + (offsets ? 1 : 0) + (depth_compare ? 1 : 0); - const u32 in_a = count <= 4 ? (count + 1) / 2 : 4; + // 0x361's handle is one more value after the coordinates: 2D.DC is (x, y | handle, + // reference), ARRAY_2D is (layer, x | y, handle); with five or more values the + // coordinates keep Ra (CUBE.LL is (x, y, z | handle, LOD)). + const u32 handle_slot = opcode == 0x361 ? 1 : 0; + const u32 count = coordinates + handle_slot + (has_lod ? 1 : 0) + (offsets ? 1 : 0) + + (depth_compare ? 1 : 0); + // Unpacked 0x361 (bit 60 clear): Ra holds the coordinates, Rb the handle and the rest. + const u32 in_a = opcode == 0x361 && !insn.Bits(60, 1) ? coordinates + : count <= 4 ? (count + 1) / 2 : opcode == 0x361 ? coordinates : 4; const auto value = [&](u32 i) -> u32 { if (i < in_a) return source_a == 255 ? 255U : source_a + i; return source_b == 255 ? 255U : source_b + (i - in_a); @@ -1547,25 +2335,40 @@ inline LoweredProgram LowerToMaxwell(std::span code) { if ((source_a != 255 && source_a + in_a > 246) || (count > in_a && source_b != 255 && source_b + (count - in_a) > 246)) return false; + const u32 texture_handle = opcode == 0x361 ? value(coordinates) : handle; + if (bindless && texture_handle >= 246) return false; + // A second descriptor buffer is read bindless: TEX.B takes the handle, loaded from + // that buffer, as its first meta register. const u32 binding = insn.Bits(54, 5); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) - return false; - result.required_texture_buffer = binding; + const bool second_buffer = !bindless && !bind_texture_buffer(binding); + const bool tex_b = bindless || second_buffer; const auto copy = [&](u32 target, u32 source) { emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); }; // Coordinates to R250.., then LOD/bias, offsets and reference to R246... for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i)); u32 meta = 246; - u32 next = coordinates; + u32 next = coordinates + handle_slot; + if (bindless) copy(meta++, texture_handle); + else if (second_buffer) + emit(0x4c98000000070000ULL | meta++ | (u64{15} << 39) | (u64{insn.Bits(40, 14)} << 20) | + (u64{binding} << 34)); if (has_lod) copy(meta++, value(next++)); if (offsets) copy(meta++, value(next++)); if (depth_compare) copy(meta++, value(next++)); + if (tex_b) { + // Maxwell TEX.B: AOFFI at 36 and the LOD mode at 37 instead of 54 and 55. + emit(0xde80000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | + (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{offsets} << 36) | + (u64{lod} << 37) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | + (u64{7} << 51)); + } else { emit(0xc000000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{meta == 246 ? 255U : 246U} << 20) | (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | (u64{7} << 51) | (u64{offsets} << 54) | (u64{lod} << 55)); + } const u32 components = static_cast(std::popcount(mask)); if (insn.Bits(79, 1)) { // FP16 results: (c0, c1) packed into the first destination, (c2, c3) into @@ -1574,7 +2377,10 @@ inline LoweredProgram LowerToMaxwell(std::span code) { if (target == 255) return; emit(0x5ca8000000000000ULL | predicate | 246 | (u64{1} << 8) | (u64{2} << 10) | (u64{low} << 20)); emit(0x5ca8000000000000ULL | predicate | 247 | (u64{1} << 8) | (u64{2} << 10) | (u64{high} << 20)); - emit(0x5c18000000000000ULL | predicate | target | (u64{246} << 8) | (u64{247} << 20) | (u64{16} << 39)); + // (hookshot: Rd = (Ra << 16) + Rb: the second of the pair, R247, is Ra. These two were the other + // way round, swapping every pair: red and green, and blue in the high half with the low one + // empty, which lost the blue of OoT's upscaler networks: the yellow cast. As Lichii has it) + emit(0x5c18000000000000ULL | predicate | target | (u64{247} << 8) | (u64{246} << 20) | (u64{16} << 39)); }; pack(static_cast(dest), 250, components > 1 ? 251U : 255U); if (components > 2) { @@ -1600,7 +2406,7 @@ inline LoweredProgram LowerToMaxwell(std::span code) { // SM70 scalar coordinates and split RG/BA destinations match TEXS. // Descriptor offsets are words in both encodings, but only native // explicitly encodes the constant-buffer binding. - if (!insn.Bits(60, 1) || (dimension != 1 && dimension != 5) || + if (bindless || !insn.Bits(60, 1) || (dimension != 1 && dimension != 5) || swizzle == ~0U || insn.Bits(76, 2) || insn.Bits(81, 3) != 7 || (lod > 1 && (lod != 3 || dimension != 1 || depth_compare)) || (lod == 3 && ((source_a & 1) || source_a >= 249)) || @@ -1609,11 +2415,11 @@ inline LoweredProgram LowerToMaxwell(std::span code) { dest == 255 || (mask & (mask - 1) && ((dest & 1) || dest >= 254)) || (split && mask == 15 && ((second_dest & 1) || second_dest >= 254)) || (depth_compare && (dimension != 1 || split || mask != 1 || (source_a & 1) || source_a >= 249)) || - (dimension == 5 && ((source_a & 1) || source_a >= 249))) { + (dimension == 5 && ((source_a & 1) || source_a >= 249)) || bindless || + !bind_texture_buffer(2)) { if (!general_tex()) return fail(index, "native texture mode or descriptor binding"); break; } - result.required_texture_buffer = 2; emit((0xd800000000000000ULL & ~(u64{insn.Bits(79, 1)} << 59)) | predicate | dest | (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(32, 8)} << 20) | (u64{insn.Bits(64, 8)} << 28) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | @@ -1653,11 +2459,12 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } break; } - case 0x307: { // FRND.F32, round to an integral floating-point value + case 0x307: case 0xb07: { // FRND.F32, round to an integral floating-point value (0xb07: cbuf) if (insn.Bits(75, 2) != 2 || insn.Bits(84, 2) != 2 || insn.Bits(81, 3) || insn.Bits(86, 6)) return fail(index, "native floating round format"); - emit(base(0x5ca8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) | - (u64{insn.Bits(32, 8)} << 20) | (u64{8 + insn.Bits(78, 2)} << 39) | + if (opcode == 0xb07 && !valid_cbuf()) return fail(index, "native constant buffer address"); + emit(base(opcode == 0xb07 ? 0x4ca8000000000000ULL : 0x5ca8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) | + (opcode == 0xb07 ? cbuf() : u64{insn.Bits(32, 8)} << 20) | (u64{8 + insn.Bits(78, 2)} << 39) | (u64{insn.Bits(80, 1)} << 44) | (u64{insn.Bits(63, 1)} << 45) | (u64{insn.Bits(62, 1)} << 49)); break; @@ -1688,15 +2495,54 @@ inline LoweredProgram LowerToMaxwell(std::span code) { result.instructions.push_back(translated); break; } - case 0x387: case 0x983: { // Captured scalar local spill at an immediate byte offset. + case 0x387: case 0x983: { // Local spill: 32, 64 or 128 bits at [Ra + immediate byte offset]. + // The size is bits 73-75 (4, 5, 6 = 32, 64, 128 bits); wider accesses become 32-bit ones. + // A load's bit 85 (.LU) is a cache hint. const bool store = opcode == 0x387; + const u32 size = insn.Bits(73, 3); const u32 data = insn.Bits(32, 8), offset = insn.Bits(40, 24); - if (insn.Bits(24, 8) != 255 || (store && (insn.Bits(16, 8) || data > 247)) || - (!store && (data || dest >= 248)) || (offset & 3) || offset > 0x7fffff || - insn.words[2] != (store ? 0x00100800U : 0x00300800U)) + u32 address = insn.Bits(24, 8); + const u32 words = size == 6 ? 4 : size == 5 ? 2 : 1, value = store ? data : static_cast(dest); + // (hookshot) LDL [Ra + URb + immediate]: bit 91 adds a uniform register at 32 (as LDS) + const bool uniform_base = !store && insn.Bits(91, 1); + const u32 base_register = uniform_base ? (insn.Bits(38, 2) ? ~0U : uniform_register(insn.Bits(32, 6))) : 255U; + if ((address != 255 && address >= 246) || (store && insn.Bits(16, 8)) || (!store && !uniform_base && data) || + (value != 255 && (value + words > 248 || (value & (words - 1)))) || (!store && value == 255) || + (offset & 3) || offset + 4 * words > 0x800000 || size < 4 || size > 6 || base_register == ~0U || + (insn.words[2] & ~(store ? 0xe00U : (0x200e00U | (uniform_base ? 0x08000000U : 0U)))) != 0x00100000U) return fail(index, "native local scalar spill mode"); - emit((store ? 0xef50000000000000ULL : 0xef40000000000000ULL) | predicate | - (store ? data : dest) | (u64{255} << 8) | (u64{offset} << 20) | (u64{4} << 48)); + if (base_register != 255) { + if (address == 255) { + address = base_register; + } else { + emit(0x5c10000000000000ULL | predicate | 250 | (u64{address} << 8) | (u64{base_register} << 20)); + address = 250; + } + } + for (u32 word = 0; word < words; ++word) + emit((store ? 0xef50000000000000ULL : 0xef40000000000000ULL) | predicate | + (value == 255 ? 255U : value + word) | (u64{address} << 8) | + (u64{offset + 4 * word} << 20) | (u64{4} << 48)); + break; + } + case 0x987: { // (hookshot) STL [Ra + URc + immediate], Rb: a uniform base at 64 (bit 91), else as 0x387 + const u32 size = insn.Bits(73, 3); + const u32 data = insn.Bits(32, 8), offset = insn.Bits(40, 24), address_register = insn.Bits(24, 8); + const u32 words = size == 6 ? 4 : size == 5 ? 2 : 1; + const u32 uniform_base = insn.Bits(91, 1) ? uniform_register(insn.Bits(64, 6)) : ~0U; + if (uniform_base == ~0U || insn.Bits(16, 8) || size < 4 || size > 6 || (offset & 3) || + offset + 4 * words > 0x800000 || (data != 255 && (data + words > 248 || (data & (words - 1)))) || + (address_register != 255 && address_register >= 246) || + (insn.words[2] & ~(0xe00U | 0x3fU)) != 0x08100000U) + return fail(index, "native local store uniform addressing mode"); + u32 address = uniform_base; + if (address_register != 255) { + emit(0x5c10000000000000ULL | predicate | 250 | (u64{address_register} << 8) | (u64{uniform_base} << 20)); + address = 250; + } + for (u32 word = 0; word < words; ++word) + emit(0xef50000000000000ULL | predicate | (data == 255 ? 255U : data + word) | (u64{address} << 8) | + (u64{offset + 4 * word} << 20) | (u64{4} << 48)); break; } case 0x803: { // P2R PR, register merge source, immediate mask, byte select (76-77) @@ -1722,8 +2568,9 @@ inline LoweredProgram LowerToMaxwell(std::span code) { result.instructions.push_back(base(0x0100000000000000ULL) | (u64{15} << 12) | (u64{insn.words[1]} << 20)); } else if (opcode == 0xc02) { - if (insn.words[1] >= 16 || insn.words[2] != 0x08000f00) return fail(index, "native uniform move source"); - emit(base(0x5c98000000000000ULL) | (u64{15} << 39) | (u64{insn.words[1] + 216} << 20)); + const u32 src = insn.words[1] < 64 ? uniform_register(insn.words[1]) : ~0U; // (hookshot: any UR) + if (src == ~0U || insn.words[2] != 0x08000f00) return fail(index, "native uniform move source"); + emit(base(0x5c98000000000000ULL) | (u64{15} << 39) | (u64{src} << 20)); } else if (opcode == 0x202) { if (insn.Bits(40, 24)) return fail(index, "native MOV register modifiers"); result.instructions.push_back(base(0x5c98000000000000ULL) | (u64{15} << 39) | @@ -1761,17 +2608,30 @@ inline LoweredProgram LowerToMaxwell(std::span code) { break; } case 0x209: case 0x809: case 0xa09: case 0xc09: { // FMNMX, including a uniform source - if (insn.Bits(74, 6) || insn.Bits(81, 6) || (opcode != 0xc09 && insn.Bits(91, 1))) + if (insn.Bits(74, 6) || insn.Bits(82, 5) || (opcode != 0xc09 && insn.Bits(91, 1))) return fail(index, "native floating min/max mode"); const u32 a = operand(0), b = operand(1); + // .NAN (bit 81): an unordered pair gives the canonical NaN. R252 = ~0 when both are numbers. + const bool propagate_nan = insn.Bits(81, 1); + if (propagate_nan) + emit(0x5800000000000000ULL | predicate | 252 | (u64{a} << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{7} << 48)); emit(base(0x5c60000000000000ULL) | (u64{a} << 8) | (u64{b} << 20) | (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42) | (u64{insn.Bits(80, 1)} << 44)); + if (propagate_nan) { + emit(base(0x5c40000000000000ULL) | (dest << 8) | (u64{252} << 20) | (u64{7} << 48)); + emit(0x0400000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53)); + emit(0x0400000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{0x7fffffffU} << 20)); + emit(base(0x5c40000000000000ULL) | (dest << 8) | (u64{252} << 20) | (u64{1} << 41) | (u64{7} << 48)); + } break; } - case 0x308: case 0xb08: { // MUFU + case 0x308: case 0xb08: case 0xd08: { // MUFU (register, constant or uniform register source) const u32 operation = insn.Bits(74, 6); - if (operation > 8 || operation == 6 || operation == 7 || insn.Bits(80, 12)) return fail(index, "native MUFU mode"); + // Bit 91 is part of the uniform-register form's encoding. + if (operation > 8 || operation == 6 || operation == 7 || + insn.Bits(80, 12) != (opcode == 0xd08 ? 0x800U : 0U)) + return fail(index, "native MUFU mode"); // SM70+ MUFU.COS/SIN take the angle in turns (compilers multiply by 1/2pi first); // Eden's Maxwell MUFU takes radians (its RRO is a plain move), so scale by 2pi. const bool turns = operation == 0 || operation == 1; @@ -1790,7 +2650,8 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(0x5080000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{operation} << 20)); emit(base(0x5ca8000000000000ULL) | (u64{1} << 8) | (u64{2} << 10) | (u64{250} << 20)); } else { - u32 src = operand(1); + u32 src = opcode == 0xd08 ? uniform_register(insn.Bits(32, 8)) : operand(1); + if (src == ~0U) return fail(index, "native MUFU uniform source"); if (turns) src = to_radians(src); emit(base(0x5080000000000000ULL) | (u64{src} << 8) | (u64{operation} << 20)); } @@ -1823,11 +2684,11 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{insn.Bits(76, 2)} << 52) | (u64{insn.Bits(78, 1)} << 55)); break; } - case 0x81a: { // SGXT.U32 Rd, Ra, n: keep the low n bits (BFE.U32 at position 0) - if (insn.words[2] || insn.words[1] > 32 || dest >= 255) + case 0x81a: { // SGXT Rd, Ra, n: the low n bits, zero- (.U32) or (hookshot) sign-extended (bit 73): BFE at 0 + if ((insn.words[2] & ~0x200U) || insn.words[1] > 32 || dest >= 255) return fail(index, "native sign extend mode"); emit(base(0x3800000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | - (u64{insn.words[1] << 8} << 20)); + (u64{insn.words[1] << 8} << 20) | (u64{insn.Bits(73, 1)} << 48)); break; } case 0x299: { // USHF by a uniform amount, one register shifted (SHL/SHR, bit 75 wraps). @@ -1835,9 +2696,13 @@ inline LoweredProgram LowerToMaxwell(std::span code) { const u32 shift = uniform_register(insn.Bits(32, 8)), high = uniform_register(insn.Bits(64, 8)); const bool right = insn.Bits(76, 1) != 0, high_result = insn.Bits(80, 1) != 0; if (dst == ~0U || dst == 255 || low == ~0U || shift == ~0U || high == ~0U || - native_predicate != 7 || insn.Bits(73, 2) != 3 || - (insn.words[2] & ~0x000118ffU) != 0x08000600U) + (native_predicate != 7 && !guarded_uniform) || (insn.words[2] & ~0x00011effU) != 0x08000000U) return fail(index, "native uniform register shift mode"); + // (hookshot) a 64-bit type, or both words: the general funnel shift (OoT's mesh programs: USHF.R.U64) + if (insn.Bits(73, 2) != 3 || !((high_result && low == 255) || (!high_result && high == 255))) { + funnel(dst, low, high, true, shift, insn.Bits(73, 2), right, high_result, insn.Bits(75, 1) != 0); + break; + } u32 source; if (high_result && low == 255) source = high; // (high:0) >> or << s, high word else if (!high_result && high == 255) source = low; // (0:low) >> or << s, low word @@ -1846,7 +2711,25 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{source} << 8) | (u64{shift} << 20) | (u64{insn.Bits(75, 1)} << 39)); break; } + case 0x499: { // (hookshot) USHF URd, URa, URc (shift), imm (the high word): as 0x419, uniform + const u32 d = uniform_register(dest), lo = uniform_register(insn.Bits(24, 8)); + const u32 shift = uniform_register(insn.Bits(64, 8)); + if (d == ~0U || d == 255 || lo == ~0U || shift == ~0U || (native_predicate != 7 && !guarded_uniform) || + (insn.words[2] & ~(0x00011effU | 0x08000000U))) + return fail(index, "native uniform funnel shift immediate mode"); + mov_imm(250, insn.words[1]); + funnel(d, lo, 250, true, shift, insn.Bits(73, 2), insn.Bits(76, 1) != 0, insn.Bits(80, 1) != 0, + insn.Bits(75, 1) != 0); + break; + } case 0x419: { // SHF.L.W.U32.HI Rd, RZ, Rb, imm (high word of imm:0 << Rb) = imm << (Rb & 31) + if ((insn.Bits(24, 8) != 255 || (insn.words[2] & ~0xffU) != 0x00010e00) && !(insn.words[2] & ~0x00011effU)) { + // (hookshot) the general form: the immediate is the high word, Rc (bits 64-71) the shift + mov_imm(250, insn.words[1]); + funnel(static_cast(dest), insn.Bits(24, 8), 250, true, insn.Bits(64, 8), insn.Bits(73, 2), + insn.Bits(76, 1) != 0, insn.Bits(80, 1) != 0, insn.Bits(75, 1) != 0); + break; + } if (insn.Bits(24, 8) != 255 || (insn.words[2] & ~0xffU) != 0x00010e00) return fail(index, "native funnel shift immediate mode"); mov_imm(250, insn.words[1]); @@ -1860,18 +2743,23 @@ inline LoweredProgram LowerToMaxwell(std::span code) { mov_imm(static_cast(dest) + 1, 0); break; } - case 0x306: case 0xd06: { // I2F.F32.U8, low byte, round-to-nearest (0xd06: uniform source) + case 0x306: case 0xd06: { // I2F.F32.U8, round-to-nearest (0xd06: uniform source) const u32 source = opcode == 0xd06 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); - if (opcode == 0x306 && insn.words[2] == 0x00101000 && !(insn.words[1] & 0xcfffff00U)) { - // I2F.U16 Rd, Rs(.H1): Maxwell's 16-bit source format, byte selector 0 or 2. - emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{1} << 10) | (u64{source} << 20) | + const u32 form16 = insn.words[2] & ~(opcode == 0xd06 ? 0x08000400U : 0x400U); + if (form16 == 0x00101000 && !(insn.words[1] & 0xcfffff00U) && source != ~0U) { + // I2F.U16 / S16 Rd, Rs(.H1): Maxwell's 16-bit source format, byte selector 0 or 2, bit 74: signed + // (hookshot: S16 and the uniform form stopped 240 of the game's programs, models and skinned meshes among them, + // from translating at all). + emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{1} << 10) | (u64{insn.Bits(74, 1)} << 13) | (u64{source} << 20) | (u64{insn.Bits(60, 2) * 2} << 41)); break; } + // I2F.U8 Rd, Rs.B0-B3: the byte selector (bits 60-61) as Maxwell's (41-42). if (insn.words[2] != (opcode == 0xd06 ? 0x08001000U : 0x1000U) || - (insn.words[1] & 0xffffff00U) || source == ~0U) + (insn.words[1] & 0xcfffff00U) || source == ~0U) return fail(index, "native byte-to-float conversion mode"); - emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{source} << 20)); + emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{source} << 20) | + (u64{insn.Bits(60, 2)} << 41)); break; } case 0x245: case 0xa45: case 0xc45: { // I2FP.F32.U32 @@ -1882,15 +2770,17 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{insn.Bits(78, 2)} << 39)); break; } - case 0x207: case 0x807: case 0xa07: - case 0x208: case 0x808: case 0xa08: { // Bitwise SEL / FSEL - const u32 a = operand(0, false), b = operand(1, false); + case 0x207: case 0x807: case 0xa07: case 0xc07: // (hookshot: 0xc07/0xc08, a uniform b) + case 0x208: case 0x808: case 0xa08: case 0xc08: { // Bitwise SEL / FSEL + // FSEL applies the floating operand modifiers (negate/absolute) before selecting. + const bool floating = (opcode & 0x1ff) == 0x008; + const u32 a = operand(0, floating), b = operand(1, floating); emit(base(0x5ca0000000000000ULL) | (u64{a} << 8) | (u64{b} << 20) | (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42)); break; } - case 0x20a: case 0x80a: case 0xa0a: { // FSET.BF, combined with Pc (bits 87-90) by bits 74-75 - if (insn.Bits(64, 8) || insn.Bits(74, 2) == 3 || insn.Bits(81, 6) || insn.Bits(91, 1)) + case 0x20a: case 0x80a: case 0xa0a: case 0xc0a: { // FSET.BF, combined with Pc (bits 87-90) by bits 74-75 (0xc0a: uniform b) + if (insn.Bits(64, 8) || insn.Bits(74, 2) == 3 || insn.Bits(81, 6) || (opcode != 0xc0a && insn.Bits(91, 1))) return fail(index, "native floating comparison output mode"); const u32 a = operand(0), b = operand(1); emit(base(0x5800000000000000ULL) | (u64{a} << 8) | (u64{b} << 20) | @@ -1898,21 +2788,76 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (u64{insn.Bits(76, 4)} << 48) | (u64{1} << 52) | (u64{insn.Bits(80, 1)} << 55)); break; } - case 0x434: { // HSETP2 with an immediate half pair: A swizzle 74-75 (neg 72, abs 73), + case 0x234: case 0x434: { // HSETP2 with a register (B swizzle 60-61, abs 62, neg 63) or an + // immediate half pair: A swizzle 74-75 (neg 72, abs 73), // combine 69-70, H_AND 71, compare 76-79, FTZ 80, P/Q 81-86, input 87-90. - mov_imm(250, insn.words[1]); + u64 source = u64{250} << 20; + if (opcode == 0x234) { + if ((insn.words[1] & 0x0fffff00U) || insn.Bits(64, 5)) + return fail(index, "native half comparison mode"); + source = (u64{insn.Bits(32, 8)} << 20) | (u64{insn.Bits(60, 2)} << 28) | + (u64{insn.Bits(62, 1)} << 30) | (u64{insn.Bits(63, 1)} << 31); + } else { + mov_imm(250, insn.words[1]); + } emit(0x5d20000000000000ULL | predicate | insn.Bits(84, 3) | (u64{insn.Bits(81, 3)} << 3) | - (u64{insn.Bits(80, 1)} << 6) | (u64{insn.Bits(24, 8)} << 8) | (u64{250} << 20) | + (u64{insn.Bits(80, 1)} << 6) | (u64{insn.Bits(24, 8)} << 8) | source | (u64{insn.Bits(76, 4)} << 35) | (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42) | (u64{insn.Bits(72, 1)} << 43) | (u64{insn.Bits(73, 1)} << 44) | (u64{insn.Bits(69, 2)} << 45) | (u64{insn.Bits(74, 2)} << 47) | (u64{insn.Bits(71, 1)} << 49)); break; } - case 0x20b: case 0x80b: case 0xa0b: // FSETP + case 0x20b: case 0x80b: case 0xa0b: case 0xc0b: // FSETP case 0x20c: case 0x80c: case 0xa0c: case 0xc0c: { // ISETP const bool floating = (opcode & 0x1ff) == 0xb; - if (!floating && insn.Bits(72, 1)) return fail(index, "native extended integer comparison"); + if (!floating && insn.Bits(72, 1)) { + // ISETP.EX (bit 72): the high-word half of a 64-bit compare. The low words' + // result comes in as the predicate at bits 68-70 (negated by 71), e.g. + // ISETP.NE P0 = lo != 0, then ISETP.NE.EX P0 = (hi != 0) OR P0. EQ: high equal + // AND low; NE: high different OR low; the orderings: high strictly ordered OR + // (high equal AND low). One or two Maxwell ISETPs into the destination; only + // the plain form (no second destination, AND with PT) is handled. + const u32 compare = insn.Bits(76, 3); + const u32 destination = insn.Bits(81, 3); + const u32 low = insn.Bits(68, 4); + // (hookshot) an accumulator predicate (bits 87-90, not PT) or boolean op (74-75): the + // compare as below, then PSETP dest = (dest op accumulator) AND PT. + const u32 accumulator = insn.Bits(87, 4), bop = insn.Bits(74, 2); + if (insn.Bits(84, 3) != 7 || bop > 2 || compare == 0 || compare == 7 || + (destination != 7 && insn.Bits(12, 3) == destination)) + return fail(index, "native extended integer comparison mode"); + // (an accumulator that is the destination: the compare into a scratch predicate, saved + // around it as MATCH does, then combined into the destination) + const bool combined = destination != 7 && ((accumulator & 7) != 7 || (accumulator & 8) || bop != 0); + u32 out = destination; + if (combined && (accumulator & 7) == destination) { + out = 6; + while (out == destination || out == (low & 7) || out == (accumulator & 7) || out == insn.Bits(12, 3)) out--; + } + if (out != destination) save_predicate(1U << out); + const u32 a = operand(0, false), b = operand(1, false); + const auto isetp = [&](u32 op, u32 input, u32 combine) { + return 0x5b60000000000000ULL | predicate | 7 | (u64{out} << 3) | + (u64{a} << 8) | (u64{b} << 20) | (u64{input & 7} << 39) | + (u64{input >> 3} << 42) | (u64{combine} << 45) | + (u64{insn.Bits(73, 1)} << 48) | (u64{op} << 49); + }; + constexpr u32 And = 0, Or = 1, Lt = 1, Eq = 2, Gt = 4, Ne = 5; + if (compare == Eq) { + emit(isetp(Eq, low, And)); + } else if (compare == Ne) { + emit(isetp(Ne, low, Or)); + } else { + emit(isetp(Eq, low, And)); + emit(isetp(compare <= 3 ? Lt : Gt, out, Or)); + } + if (combined) + emit(0x5090000000000000ULL | predicate | (u64{destination} << 3) | 7 | (u64{out} << 12) | + (u64{bop} << 24) | (u64{accumulator} << 29) | (u64{7} << 39)); + if (out != destination) restore_predicate(1U << out); + break; + } const u32 a = operand(0, floating), b = operand(1, floating); const u32 combine = insn.Bits(74, 2); if (combine > 2) return fail(index, "native predicate combination"); @@ -1960,15 +2905,19 @@ inline LoweredProgram LowerToMaxwell(std::span code) { } case 0x217: case 0x817: case 0xa17: case 0xc17: { // IMNMX register, immediate, cbuf or uniform operand. if ((insn.words[2] & ~(0x04000200U | (opcode == 0xc17 ? 0x08000000U : 0U))) != 0x03800000 || - (opcode != 0xa17 && insn.words[1] > (opcode == 0x817 ? 0x7ffffU : 255U)) || + (opcode == 0x817 && insn.words[1] > 0x7ffffU && insn.words[1] < 0xfff80000U) || + (opcode != 0xa17 && opcode != 0x817 && insn.words[1] > 255U) || (opcode == 0xa17 && (!valid_cbuf() || insn.Bits(62, 2)))) return fail(index, "native integer min/max mode"); + // An immediate is a signed 20-bit value: 19 bits at 20, the sign in bit 56. const u32 source = opcode == 0xc17 ? uniform_register(insn.words[1]) : opcode == 0xa17 ? operand(1, false) + : opcode == 0x817 ? insn.words[1] & 0x7ffffU : insn.words[1]; if (source == ~0U) return fail(index, "native uniform min/max operand"); emit(base(opcode == 0x817 ? 0x3820000000000000ULL : 0x5c20000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{source} << 20) | + (u64{opcode == 0x817 ? insn.words[1] >> 31 : 0U} << 56) | (u64{insn.Bits(87, 4)} << 39) | (u64{insn.Bits(73, 1)} << 48)); break; } @@ -1985,6 +2934,12 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(base(0x3828000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{insn.words[1]} << 20)); break; } + if (((insn.Bits(73, 2) != 3 && !is_signed) || insn.Bits(75, 1) || !insn.Bits(80, 1) || insn.words[1] > 32) && + !(insn.words[2] & ~0x00011effU) && insn.words[1] < 64) { // (hookshot) the general funnel shift + if (funnel(static_cast(dest), insn.Bits(24, 8), insn.Bits(64, 8), false, insn.words[1], + insn.Bits(73, 2), insn.Bits(76, 1) != 0, insn.Bits(80, 1) != 0, insn.Bits(75, 1) != 0)) + break; + } if ((insn.Bits(73, 2) != 3 && !is_signed) || insn.Bits(75, 1) || !insn.Bits(80, 1) || insn.words[1] > 32) return fail(index, "native funnel-shift mode"); const u32 low = insn.Bits(24, 8), high = insn.Bits(64, 8), shift = insn.words[1]; if (insn.Bits(76, 1)) { @@ -1998,6 +2953,20 @@ inline LoweredProgram LowerToMaxwell(std::span code) { break; } case 0xa11: { // LEA with constant base and carry predicate. + if ((insn.words[2] & ~(0x07800000U | 0x0000f800U)) == 0x000f06ffU && !insn.Bits(62, 2) && + insn.Bits(75, 5) != 0) { + // LEA.HI.X.SX32, no high source: (sign-extended Ra << shift) >> 32, plus the + // constant high word and the carry predicate (bits 87-90). + const u32 b = operand(1, false); + emit(0x3828000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) | + (u64{32 - insn.Bits(75, 5)} << 20) | (u64{1} << 48)); + mov_imm(252, 1); + emit(0x5ca0000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{255} << 20) | + (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42)); + emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{b} << 20)); + emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20)); + break; + } if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) || insn.Bits(81, 3) == 7 || insn.Bits(62, 2)) return fail(index, "native constant LEA carry mode"); @@ -2008,35 +2977,80 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (dest << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49)); break; } - case 0x211: case 0x811: { // LEA low word, no carry result + case 0x211: case 0x811: case 0xc11: { // LEA low word (0xc11: uniform Rb) if (insn.Bits(80, 1)) { - // LEA.HI Rd, Ra, Rb (or an immediate, 0x811), RZ, s: Rb + high word of (Ra << s); - // bit 73 sign-extends Ra. - if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(81, 3) != 7 || - insn.Bits(64, 8) != 255 || (opcode == 0x211 && insn.Bits(62, 2))) + // LEA.HI Rd, Ra, Rb (or an immediate, 0x811), Rc, s: Rb + high word of (Rc:Ra << s); + // bit 73 sign-extends Ra (Rc is RZ) and .X (bit 74) adds the carry predicate in bits 87-90. + const bool extended = insn.Bits(74, 1) != 0; + const u32 high = insn.Bits(64, 8); + // (hookshot) bit 63 negates Rb, as in the low form (Maxwell IADD's bit 48); not with .X + const bool negate_b = opcode == 0x211 && insn.Bits(63, 1); + if (insn.Bits(72, 1) || insn.Bits(81, 3) != 7 || + (high != 255 && insn.Bits(73, 1)) || (opcode == 0x211 && insn.Bits(62, 1)) || + (negate_b && extended) || (opcode == 0xc11 && (insn.words[1] & ~0x3fU))) return fail(index, "native high effective address mode"); - u32 b = insn.Bits(32, 8); + u32 b = opcode == 0xc11 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); + if (b == ~0U) return fail(index, "native uniform LEA register"); if (opcode == 0x811) { b = insn.words[1] ? 251U : 255U; if (b == 251) mov_imm(251, insn.words[1]); } const u64 shift = (u64{32 - insn.Bits(75, 5)} << 20) | (u64{insn.Bits(73, 1)} << 48); - if (b == 255) { - emit(base(0x3828000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | shift); + // The high word is a right shift of Ra, or with Rc a left funnel shift (SHF.L). + const auto high_word = [&](u64 target) { + if (high == 255) emit(target | 0x3828000000000000ULL | (u64{insn.Bits(24, 8)} << 8) | shift); + else emit(target | 0x36f8000000000000ULL | (u64{insn.Bits(24, 8)} << 8) | (u64{high} << 39) | + (u64{insn.Bits(75, 5)} << 20)); + }; + if (b == 255 && !extended) { + high_word(base(0)); } else { - emit(0x3828000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) | shift); - emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{b} << 20)); + high_word(predicate | 250); + if (extended) { + mov_imm(252, 1); + emit(0x5ca0000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{255} << 20) | + (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42)); + emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{252} << 20)); + } + emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{b} << 20) | (u64{negate_b} << 48)); } break; } - if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) || insn.Bits(81, 3) != 7 || - (opcode == 0x211 && insn.Bits(62, 1))) return fail(index, "native LEA carry/high mode"); + // A carry out (bits 81-83) is the low half of a 64-bit address; IADD3.X adds it next. + const u32 carry_out = insn.Bits(81, 3); + if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) || + (carry_out != 7 && ((opcode != 0x811 && insn.Bits(63, 1)) || + (rewritten_uniform && (opcode == 0xc11 || + (guarded_uniform && 6 - carry_out == guard_slot))))) || + (opcode == 0x211 && insn.Bits(62, 1)) || (opcode == 0xc11 && (insn.words[1] & ~0x8000003fU))) + return fail(index, "native LEA carry/high mode"); // 0x211 bit 63 negates Rb (LEA Rd, Ra, -Rb, s), as Maxwell ISCADD's bit 48. - u32 b = insn.Bits(32, 8); + u32 b = opcode == 0xc11 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); + if (b == ~0U) return fail(index, "native uniform LEA register"); if (opcode == 0x811) { mov_imm(251, insn.words[1]); b = 251; } + if (carry_out != 7) { + // The sum in R250, carry = sum < Rb (unsigned), as IADD3's carry out; a uniform + // predicate goes through P(6-n) into R239. + const u32 target = rewritten_uniform ? 6 - carry_out : carry_out, mask = 1U << target; + // (hookshot) guarded (a uniform guard swapped in, its own save in R253): the guest P(6-n) kept in R252, + // and the UPn started from R239, so that a false guard leaves it as it was + const u32 keep = guarded_uniform ? 252U : 253U; + emit(0x5c18000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) | + (u64{b} << 20) | (u64{insn.Bits(75, 5)} << 39)); + if (rewritten_uniform) emit(0x38e8000000070000ULL | keep | (u64{255} << 8) | (u64{mask} << 20)); + if (rewritten_uniform && guarded_uniform) emit(0x38f0000000070000ULL | (u64{239} << 8) | (u64{mask} << 20)); + emit(0x5b60000000000000ULL | predicate | 7 | (u64{target} << 3) | (u64{250} << 8) | + (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49)); + if (rewritten_uniform) { + emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{mask} << 20)); + emit(0x38f0000000070000ULL | (u64{keep} << 8) | (u64{mask} << 20)); + } + emit(base(0x5c98000000000000ULL) | (u64{15} << 39) | (u64{250} << 20)); + break; + } emit(base(0x5c18000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{b} << 20) | (u64{insn.Bits(75, 5)} << 39) | - (u64{opcode == 0x211 ? insn.Bits(63, 1) : 0U} << 48)); + (u64{opcode != 0x811 ? insn.Bits(63, 1) : 0U} << 48)); break; } case 0x210: case 0x810: case 0xa10: case 0xc10: { // IADD3 with zero third source and no carry outputs @@ -2044,13 +3058,18 @@ inline LoweredProgram LowerToMaxwell(std::span code) { // a carry-in predicate (bits 87-89, negate 90). Uniform forms keep UPn in R239 bit 6-n. if ((opcode == 0x210 || opcode == 0x810) && insn.Bits(64, 8) == 255 && !insn.Bits(73, 1) && !insn.Bits(75, 2) && insn.Bits(84, 3) == 7 && insn.Bits(77, 4) == 15 && !insn.Bits(72, 1) && - (insn.Bits(81, 3) != 7 || insn.Bits(74, 1))) { + (insn.Bits(81, 3) != 7 || insn.Bits(74, 1)) && (opcode == 0x810 || !insn.Bits(63, 1))) { const bool carry_in = insn.Bits(74, 1) != 0; const u32 carry_out = insn.Bits(81, 3); const u32 a = insn.Bits(24, 8); + // (hookshot) any UPn (it was UP0/UP1): its slot P(6-n) is saved and restored around the op; only a + // slot that is also the uniform guard's could not be + const bool slot_conflict = guarded_uniform && + ((carry_out != 7 && 6 - carry_out == guard_slot) || + (carry_in && insn.Bits(87, 3) != 7 && 6 - insn.Bits(87, 3) == guard_slot)); if ((carry_in && carry_out != 7) || (opcode == 0x210 && (insn.words[1] & ~0xffU)) || - (rewritten_uniform && carry_out != 7 && carry_out > 1) || - (rewritten_uniform && carry_in && insn.Bits(87, 3) > 1 && insn.Bits(87, 3) != 7)) + (rewritten_uniform && carry_out != 7 && carry_out > 6) || + (rewritten_uniform && carry_in && insn.Bits(87, 3) > 6 && insn.Bits(87, 3) != 7) || slot_conflict) return fail(index, "native integer carry mode"); if (opcode == 0x810) { emit(0x1c00000000000000ULL | predicate | 250 | (u64{a} << 8) | (u64{insn.words[1]} << 20)); @@ -2087,9 +3106,65 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{251} << 20)); break; } - if (opcode == 0xa10 && insn.Bits(81, 3) != 7) { + // Three sources, negations or two carries: the sum builds up in R253 (.CC) with the carry + // count in R254 (.X). Pu/Pv (bits 81/84) are set when the count reaches 1/2, and IADD3.X + // adds both carry-ins (bits 87, 77). A negated source is ~x, plus 1 unless extended. + const bool extended = insn.Bits(74, 1) != 0; + const bool negate_a = insn.Bits(72, 1), negate_b = opcode != 0x810 && insn.Bits(63, 1), negate_c = insn.Bits(75, 1); + if ((extended || insn.Bits(81, 6) != 0x3f) && + (insn.Bits(64, 8) != 255 || insn.Bits(84, 3) != 7 || negate_a || negate_b || negate_c || + (extended && insn.Bits(77, 4) != 15))) { + const u32 first_output = insn.Bits(81, 3), second_output = insn.Bits(84, 3); + if (rewritten_uniform || guarded_uniform || insn.Bits(73, 1) || insn.Bits(76, 1) || + (first_output != 7 && first_output == second_output) || + (!extended && (insn.Bits(77, 4) != 15 || insn.Bits(87, 4) != 15)) || + (opcode == 0x210 && (insn.words[1] & ~0x800000ffU)) || (opcode == 0xa10 && insn.Bits(62, 1)) || + (opcode == 0xc10 && (insn.words[1] & ~0x8000003fU))) + return fail(index, "native three-input carry mode"); + u32 a = insn.Bits(24, 8), b = operand(1, false), c = insn.Bits(64, 8); + const auto invert = [&](u32& source, u32 target) { + emit(0x0400000000000000ULL | predicate | target | (u64{source} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53)); + source = target; + }; + if (negate_a) invert(a, 250); + if (negate_b) invert(b, 251); + if (negate_c) invert(c, 252); + const auto accumulate = [&](u32 source) { + emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{source} << 20) | (u64{1} << 47)); + emit(0x5c10000000000000ULL | predicate | 254 | (u64{254} << 8) | (u64{255} << 20) | (u64{1} << 43)); + }; + emit(0x5c10000000000000ULL | predicate | 253 | (u64{a} << 8) | (u64{b} << 20) | (u64{1} << 47)); + emit(0x5c10000000000000ULL | predicate | 254 | (u64{255} << 8) | (u64{255} << 20) | (u64{1} << 43)); + accumulate(c); + if (extended) { + for (const u32 carry : {insn.Bits(87, 4), insn.Bits(77, 4)}) { + if (carry == 15) continue; + mov_imm(250, 1); + if (carry != 7) + emit(0x5ca0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | + (u64{carry & 7} << 39) | (u64{carry >> 3} << 42)); + accumulate(250); + } + } else if (const u32 ones = u32{negate_a} + u32{negate_b} + u32{negate_c}) { + mov_imm(250, ones); + accumulate(250); + } + if (dest != 255) emit(base(0x5c98000000000000ULL) | (u64{253} << 20) | (u64{15} << 39)); + // An output that is also the guard predicate is written last. + const bool guard_first = (native_predicate & 7) != 7 && first_output == (native_predicate & 7); + for (const u32 output : {guard_first ? 1U : 0U, guard_first ? 0U : 1U}) { + const u32 target = output ? second_output : first_output; + if (target == 7) continue; + mov_imm(250, output + 1); + emit(0x5b60000000000000ULL | predicate | 7 | (u64{target} << 3) | (u64{254} << 8) | + (u64{250} << 20) | (u64{7} << 39) | (u64{6} << 49)); + } + break; + } + if ((opcode == 0xa10 || opcode == 0xc10) && insn.Bits(81, 3) != 7) { // cbuf or uniform b if (insn.Bits(64, 8) != 255 || insn.Bits(73, 4) || insn.Bits(84, 7) != 0x7f || - insn.Bits(77, 4) != 15 || insn.Bits(63, 1) || insn.Bits(72, 1)) + insn.Bits(77, 4) != 15 || insn.Bits(63, 1) || insn.Bits(72, 1) || + (opcode == 0xc10 && (insn.words[1] & ~0x3fU))) return fail(index, "native constant add carry mode"); const u32 b = operand(1, false); emit(base(0x5c10000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{b} << 20)); @@ -2097,13 +3172,15 @@ inline LoweredProgram LowerToMaxwell(std::span code) { (dest << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49)); break; } - if (opcode == 0xa10 && insn.Bits(74, 1)) { - if (insn.Bits(24, 8) != 255 || (insn.words[2] & ~0x07800000U) != 0x007fe4ffU) + if ((opcode == 0xa10 || opcode == 0xc10) && insn.Bits(74, 1)) { // IADD3.X Ra + cbuf or UR + carry predicate + if ((insn.words[2] & ~(0x07800000U | (opcode == 0xc10 ? 0x08000000U : 0U))) != 0x007fe4ffU || + (opcode == 0xc10 && (insn.words[1] & ~0x3fU))) return fail(index, "native extended constant add mode"); - const u32 b = operand(1, false); + const u32 a = insn.Bits(24, 8), b = operand(1, false); mov_imm(250, 1); emit(0x5ca0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42)); + if (a != 255) emit(0x5c10000000000000ULL | predicate | 250 | (u64{a} << 8) | (u64{250} << 20)); emit(base(0x5c10000000000000ULL) | (u64{b} << 8) | (u64{250} << 20)); break; } @@ -2156,18 +3233,49 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(0xe24000000000000fULL | predicate); break; } + // (hookshot) the link's MOV (the return PC into Rlink) up to sixteen instructions back, those between + // writing other registers and not branching (OoT's mesh programs set it ten instructions before) + // (hookshot: up to 48: OoT's foliage mesh programs set it eighteen instructions before) + u32 setup = index; + for (u32 back = 1; back <= 48 && back <= index; ++back) { + const Instruction& prior = code[index - back]; + if ((prior.words[0] & ~0x00ff0000U) == 0x00007802 && prior.Bits(16, 8) < 250 && + prior.words[1] == (index + 1) * 16 && prior.words[2] == 0xf00) { + setup = index - back; + break; + } + const u32 prior_op = prior.Bits(0, 12); + // (predicated ones may lie between: the clobber check below sees their writes) + if (prior_op >= 0x940 && prior_op <= 0x95f) break; + } + // (hookshot) what a write may cover, with the wider window: register pairs and quads of the wide + // multiplies, loads and texture fetches (their second destination at 64 too) + const auto writes_link = [&](const Instruction& prior, u32 link_reg) { + const u32 op = prior.Bits(0, 12), d = prior.Bits(16, 8); + u32 span = 1; + bool second = false; + if (op == 0x225 || op == 0x625 || op == 0x825 || op == 0x805) span = 2; + else if (op == 0x381 || op == 0x981 || op == 0x983 || op == 0x984 || op == 0x980) { + const u32 size = prior.Bits(73, 3); + span = size == 6 ? 4 : size == 5 ? 2 : 1; + } else if ((op & 0x1f0) == 0x160) { + span = 4; + second = true; + } + const auto covers = [&](u32 r) { return r != 255 && link_reg >= r && link_reg < r + span; }; + return covers(d) || (second && covers(prior.Bits(64, 8))); + }; + bool clobbered = setup == index; + for (u32 k = setup + 1; !clobbered && k < index; ++k) + clobbered = writes_link(code[k], code[setup].Bits(16, 8)); if (main_exited || index == 0 || insn.words[0] != 0x7944 || - (insn.words[2] & ~0x3ffffU) != 0x03c00000 || - (code[index - 1].words[0] & ~0x00ff0000U) != 0x00007802 || - code[index - 1].Bits(16, 8) >= 250 || - code[index - 1].words[1] != (index + 1) * 16 || - code[index - 1].words[2] != 0xf00) + (insn.words[2] & ~0x3ffffU) != 0x03c00000 || clobbered) return fail(index, "native call linkage mode"); const u64 raw = insn.words[1] | (u64{insn.words[2] & 0x3ffff} << 32); const s64 relative = static_cast(raw << 14) >> 14; const s64 target = s64{index} * 16 + 16 + relative; if (target < 0 || target % 16) return fail(index, "native call destination alignment"); - const u32 link = code[index - 1].Bits(16, 8); + const u32 link = code[setup].Bits(16, 8); for (size_t call = 0; call < call_targets.size(); ++call) { if (call_targets[call] == target / 16 && call_links[call] != link) return fail(index, "native inconsistent call linkage"); @@ -2191,9 +3299,15 @@ inline LoweredProgram LowerToMaxwell(std::span code) { return false; }; bool linked = active_link && return_reg == *active_link; + // (hookshot) the stores write no register: their bits 16-23 are no destination (OoT's mesh programs spill + // between the copy and the RET) + const auto is_store = [](u32 op) { + return op == 0x385 || op == 0x386 || op == 0x387 || op == 0x388 || op == 0x985 || op == 0x986 || + op == 0x987 || op == 0x988; + }; for (u32 prior = index; active_link && !linked && prior-- > active_entry;) { if (copies_link(code[prior])) linked = true; - else if (code[prior].Bits(16, 8) == return_reg) break; + else if (!is_store(code[prior].Bits(0, 12)) && code[prior].Bits(16, 8) == return_reg) break; } if (!main_exited || !linked || (insn.words[0] & 0x00ffffffU) != 0x00007950 || return_reg >= 249 || @@ -2236,6 +3350,24 @@ inline LoweredProgram LowerToMaxwell(std::span code) { return finish(index); // the closing self-branch break; } + case 0x923: case 0x927: { // ISBERD / ISBEWR: task and mesh programs' stage buffer. + // Ampere: Ra 24-31, offset 40-63, ISBERD dest 16-23, ISBEWR data 32-39; 75 = .32 (else + // U8), 76-77 = 3 .ATTR (0 the map region), 78 = .SKEW, 79 = .O (the output buffer). + // Kept as an nx2 pseudo instruction (Maxwell 0xefc0, free there) that the frontend + // reads for the task or mesh stage: data/dest 0-7, Ra 8-15, offset 20-43, .32 44, + // .ATTR 45, .SKEW 46, .O 47, write 48. Addresses are linear bytes in the region. + const bool write = opcode == 0x927; + const u32 region = insn.Bits(76, 2); + const bool output = insn.Bits(79, 1) != 0; + if ((insn.words[2] & ~0xf800U) != 0 || (region != 0 && region != 3) || write != output || + (!write && region != 3) || (!write && dest == 255)) + return fail(index, "native stage buffer access mode"); + emit(0xefc0000000000000ULL | predicate | (write ? u64{insn.Bits(32, 8)} : dest) | + (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(40, 24)} << 20) | + (u64{insn.Bits(75, 1)} << 44) | (u64{region == 3} << 45) | + (u64{insn.Bits(78, 1)} << 46) | (u64{output} << 47) | (u64{write} << 48)); + break; + } case 0x94d: { // EXIT if (insn.Bits(84, 2)) { return fail(index, "native conditional/special EXIT"); @@ -2277,13 +3409,96 @@ inline LoweredProgram LowerToMaxwell(std::span code) { return fail(index, "unsupported native opcode"); } if (!operand_error.empty()) return fail(index, operand_error); + if (f16_components != 0) { + // Each pair as F2FP.PACK_AB: the second value in the high half. + const u64 pred = u64{insn.Bits(12, 4)} << 16; + const auto pack = [&](u32 target, u32 low, u32 high) { + result.instructions.push_back(0x5ca8000000000000ULL | pred | 250 | (u64{1} << 8) | + (u64{2} << 10) | (u64{high} << 20)); + result.instructions.push_back(0x5ca8000000000000ULL | pred | 251 | (u64{1} << 8) | + (u64{2} << 10) | (u64{low} << 20)); + result.instructions.push_back(0x5c18000000000000ULL | pred | target | (u64{250} << 8) | + (u64{251} << 20) | (u64{16} << 39)); + }; + if (f16_dest != 255) pack(f16_dest, 248, f16_components > 1 ? 249U : 255U); + if (f16_second != 255 && f16_components > 2) + pack(f16_second, 246, f16_components > 3 ? 247U : 255U); + } if (output_mask) { emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{output_mask} << 20)); emit(0x38f0000000070000ULL | (u64{254} << 8) | (u64{output_mask} << 20)); } if (guarded_uniform) restore_predicate(guard_mask); + for (u32 r = 0; r < 255; ++r) result.used_reserved[r] = result.used_reserved[r] || lowering_used[r]; + if (any_high) { + // A reserved number the lowering itself used here as well as the native high GPR: no telling them apart. + // The scratch registers and R239 are named by the lowering's own words, so an instruction that lowered to + // more than one word is taken to use them + const size_t words = result.instructions.size() - words_before; + bool conflict = (high_in_scratch || high_239) && words > 1; + for (u32 r = 0; r < 255 && !conflict; ++r) conflict = (native_high[r] || marker_taken[r]) && lowering_used[r]; + if (conflict && !survey) return fail(index, "native program uses lowering scratch registers"); + RegisterRun runs[24]; + u32 nruns = 0; + for (u32 r = 0; r < nrenamed; ++r) runs[nruns++] = renamed[r]; + for (u32 r = 0; r < 255; ++r) { + if (!native_high[r]) continue; + RegisterRun& last = runs[nruns ? nruns - 1 : 0]; + if (nruns > nrenamed && last.number + last.length == r && last.length < 4) last.length++; + else if (nruns < 24) runs[nruns++] = {r, r, 1}; + } + std::vector prefix; + for (u32 i = 0; i < nruns; i += 2) prefix.push_back(RegisterMapPrefix(runs + i, (std::min)(2U, nruns - i))); + std::vector words_out; + words_out.reserve(words * (prefix.size() + 1)); + std::vector moved(words); // (each word's new place, for the relocations made here) + result.register_map_words += static_cast(prefix.size() * words); + for (size_t w = 0; w < words; ++w) { + words_out.insert(words_out.end(), prefix.begin(), prefix.end()); + moved[w] = static_cast(words_before + words_out.size()); + words_out.push_back(result.instructions[words_before + w]); + } + result.instructions.resize(words_before); + result.instructions.insert(result.instructions.end(), words_out.begin(), words_out.end()); + for (size_t r = relocations_before; r < relocations.size(); ++r) + relocations[r].output_index = moved[relocations[r].output_index - words_before]; + } } return fail(static_cast(code.size()), main_exited ? "native program lacks captured library return" : "native program has no unconditional EXIT"); } +inline LoweredProgram LowerToMaxwellRules(std::span code); +// (hookshot) as before; a program that fails is tried again with the extended renaming (extended_renaming above) +inline LoweredProgram LowerToMaxwell(std::span code) { + extended_renaming = false; + LoweredProgram lowered = LowerToMaxwellRules(code); + if (lowered) return lowered; + extended_renaming = true; + LoweredProgram again = LowerToMaxwellRules(code); + extended_renaming = false; + return again ? again : lowered; +} + +inline LoweredProgram LowerToMaxwellRules(std::span code) { + // Programs using uniform registers above UR27 are lowered again with mirrors for them. + u32 high_uniforms = 0; + std::array avoid{}; + bool surveyed = false; + for (;;) { + LoweredProgram lowered = LowerToMaxwellWith(code, high_uniforms, avoid); + if (!lowered && lowered.high_uniform_registers > high_uniforms) { + high_uniforms = lowered.high_uniform_registers; + continue; + } + // (hookshot) a renamed register's marker met a uniform register the lowering used later: the uniform numbers + // the whole program uses first (a pass that goes on past those conflicts), then again + if (!lowered && !surveyed && lowered.error == std::string_view{"native program uses lowering scratch registers"}) { + surveyed = true; + avoid = LowerToMaxwellWith(code, high_uniforms, {}, true).used_reserved; + continue; + } + return lowered; + } +} + } // namespace Shader::Ampere diff --git a/host/hos_main.c b/host/hos_main.c index 7904a8eea..375af5264 100644 --- a/host/hos_main.c +++ b/host/hos_main.c @@ -28,11 +28,11 @@ #include #ifdef __linux__ #include +#include #endif #include #include #include -#include #endif /* (linux) the separator of the paths the executable's folder is joined with */ #ifdef _WIN32 @@ -139,10 +139,11 @@ static int reserve_guest(void) { hos_commit_init(); return VirtualAlloc((void *)A64_HOST_BASE, GUEST_SPACE, MEM_RESERVE | watch, PAGE_NOACCESS) == (void *)A64_HOST_BASE; #else - /* (linux) the whole window readable and writable at once, its pages made as they are first touched (no commit + /* (POSIX) the whole window readable and writable at once, its pages made as they are first touched (no commit step: MAP_NORESERVE). Decommitting is madvise (host/posix/win32_posix.c VirtualFree): the pages read as zero. */ void hos_commit_init(void); hos_commit_init(); /* (the crash report) */ +#ifdef __linux__ /* The window is a memfd's, shared, so the Vulkan back end can hand the GPU the game's own pages as a dma-buf (udmabuf, gpu/vk/vk_memory.cpp import_dma_buf) instead of a host pointer: amdgpu checks every page of a host-pointer import again at each command submission, 15-45 ms a submission in Kokiri Forest. Pages of a shared mapping are freed @@ -187,14 +188,23 @@ static int reserve_guest(void) { hos_log("mem: no memfd for the guest window (errno %d): anonymous memory", errno); } } - if (mmap((void *)A64_HOST_BASE, GUEST_SPACE, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE | MAP_NORESERVE, -1, 0) != (void *)A64_HOST_BASE) +#endif + void *guest = mmap((void *)A64_HOST_BASE, GUEST_SPACE, PROT_READ | PROT_WRITE, +#ifdef __linux__ + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE | MAP_NORESERVE, +#else + MAP_PRIVATE | MAP_ANONYMOUS, +#endif + -1, 0); + if (guest != (void *)A64_HOST_BASE) { + if (guest != MAP_FAILED) munmap(guest, GUEST_SPACE); return 0; + } /* (issue 312) no transparent huge pages in the guest window: the renderer imports its pools as userptr memory, and every collapse khugepaged makes in them changes their mappings, which the kernel answers by pinning the pages again at the next submission: ~250 ms a submit on RADV while the GPU sat idle. HOS_THP=1: as before */ - if (!getenv("HOS_THP")) madvise((void *)A64_HOST_BASE, GUEST_SPACE, MADV_NOHUGEPAGE); #ifdef __linux__ + if (!getenv("HOS_THP")) madvise((void *)A64_HOST_BASE, GUEST_SPACE, MADV_NOHUGEPAGE); /* (HOS_NO_THP=1, experiment: none outside the window either. With THP "always" 400-450 MB of the host heap's and the binary's bss are huge pages, each resident whole when any of its 2 MB is touched) */ if (getenv("HOS_NO_THP")) prctl(PR_SET_THP_DISABLE, 1, 0, 0, 0); @@ -209,7 +219,12 @@ static int reserve_guest(void) { int hos_guest_memfd = -1; int hos_posix_guest_memfd(void) { return hos_guest_memfd; } int hos_posix_guest_discard(void *p, size_t n) { +#ifdef __linux__ return madvise(p, n, hos_guest_memfd >= 0 ? MADV_REMOVE : MADV_DONTNEED); +#else + memset(p, 0, n); + return madvise(p, n, MADV_DONTNEED); +#endif } #endif @@ -378,6 +393,30 @@ static void load_hookshot_env(void) { fclose(f); } +/* (macOS port) HOS_QUALITY= (hookshot_env.txt): the Insert menu's quality presets, for the WebGPU renderer, which + has no menu. Each sets only what the player did not set: high/alta, the game's own (docked, dynamic resolution, FSR); + medium/media, a fixed 75% resolution; low/baixa, the handheld profile (the game's lighter resolutions); minimum/minima, + handheld at a fixed 50%, the plain upscale. */ +#ifndef _WIN32 /* (Windows has the Insert menu) */ +static void apply_quality_preset(void) { + const char *q = getenv("HOS_QUALITY"); + if (!q || !*q) return; + const int medium = !strcmp(q, "medium") || !strcmp(q, "media") || !strcmp(q, "média"); + const int low = !strcmp(q, "low") || !strcmp(q, "baixa"); + const int minimum = !strcmp(q, "minimum") || !strcmp(q, "minima") || !strcmp(q, "mínima"); + const int high = !strcmp(q, "high") || !strcmp(q, "alta"); + if (!medium && !low && !minimum && !high) { + hos_log("quality: HOS_QUALITY=%s is none of high (alta), medium (media), low (baixa), minimum (minima): ignored", q); + return; + } + if (medium) setenv("HOS_RES_SCALE", "0.75", 0); + if (low || minimum) setenv("HOS_HANDHELD", "1", 0); + if (minimum) setenv("HOS_RES_SCALE", "0.5", 0), setenv("RX_FSR", "0", 0); + hos_log("quality: %s (handheld %s, resolution %s, FSR %s)", q, getenv("HOS_HANDHELD") ? getenv("HOS_HANDHELD") : "0", + getenv("HOS_RES_SCALE") ? getenv("HOS_RES_SCALE") : "dynamic", getenv("RX_FSR") ? getenv("RX_FSR") : "1"); +} +#endif + int hos_oot_protect_hearts; volatile int hos_oot_live_action; /* (test runs) the live input file's "!cheat ": host/nn_hid.c */ @@ -1397,6 +1436,7 @@ int main(int argc, char **argv) { int hos_display_init(void); void hos_display_loop(void); load_hookshot_env(); + apply_quality_preset(); game_argc = argc, game_argv = argv; const int window = hos_display_init(); HANDLE t = CreateThread(NULL, 64u << 20, game_thread, NULL, 0, NULL); diff --git a/host/hos_paths.h b/host/hos_paths.h index 5bf7cc5df..4d932ec5a 100644 --- a/host/hos_paths.h +++ b/host/hos_paths.h @@ -6,15 +6,11 @@ #pragma once #include #include -#ifdef _WIN32 -#ifndef NOMINMAX -#define NOMINMAX -#endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include - +#ifdef _WIN32 static inline DWORD hos_game_path_a(char *buf, DWORD size) { DWORD n = GetModuleFileNameA(NULL, buf, size); if (!n || n >= size) return n; diff --git a/host/hos_sdl.c b/host/hos_sdl.c index 964718976..95cfec570 100644 --- a/host/hos_sdl.c +++ b/host/hos_sdl.c @@ -15,7 +15,7 @@ static struct { /* window */ void *CreateWindow, *SetWindowPosition, *SetWindowFullscreen, *SetRelativeMouseMode, *GetPrimaryDisplay, *GetDesktopDisplayMode, *Vulkan_CreateSurface, *PixelSize, *GetKeyboardState, *GetMouseState, *WaitEventTimeout, - *PollEvent, *GetWindowSize, *LoadPNG, *SetWindowIcon, *DestroySurface; + *PollEvent, *GetWindowSize, *LoadPNG, *SetWindowIcon, *DestroySurface, *Metal_CreateView, *Metal_GetLayer; /* sound */ void *OpenAudioDevice, *PauseAudioDevice, *OpenAudioDeviceStream, *ResumeAudioStreamDevice, *PutAudioStreamData; /* controllers */ @@ -43,8 +43,14 @@ static int get_all(HMODULE m, int three) { GET(SetWindowFullscreen, "SDL_SetWindowFullscreen"); GET2(SetRelativeMouseMode, "SDL_SetWindowRelativeMouseMode", "SDL_SetRelativeMouseMode"); GET(GetDesktopDisplayMode, "SDL_GetDesktopDisplayMode"); +#ifdef __APPLE__ + GET(Metal_CreateView, "SDL_Metal_CreateView"); + GET(Metal_GetLayer, "SDL_Metal_GetLayer"); + GET2(PixelSize, "SDL_GetWindowSizeInPixels", "SDL_GetWindowSizeInPixels"); +#else GET(Vulkan_CreateSurface, "SDL_Vulkan_CreateSurface"); GET2(PixelSize, "SDL_GetWindowSizeInPixels", "SDL_Vulkan_GetDrawableSize"); +#endif GET(GetKeyboardState, "SDL_GetKeyboardState"); GET(GetMouseState, "SDL_GetMouseState"); GET(GetWindowSize, "SDL_GetWindowSize"); @@ -127,13 +133,21 @@ void hos_sdl_identity(const char *name, const char *app_id) { const char *hos_sdl_error(void) { return version ? CALL(const char *(*)(void), GetError)() : "no SDL"; } /* ---- the window ---- */ -enum { WIN_SHOWN2 = 0x4, WIN_RESIZABLE = 0x20, WIN_HIGH_DENSITY = 0x2000, WIN_VULKAN = 0x10000000, CENTRED = 0x2FFF0000 }; +enum { + WIN_SHOWN2 = 0x4, WIN_RESIZABLE = 0x20, WIN_HIGH_DENSITY = 0x2000, WIN_VULKAN = 0x10000000, + WIN_METAL = 0x20000000, CENTRED = 0x2FFF0000 +}; void *hos_sdl_window(const char *title, int width, int height) { +#ifdef __APPLE__ + const uint64_t flags = WIN_RESIZABLE | WIN_HIGH_DENSITY | WIN_METAL; +#else + const uint64_t flags = WIN_RESIZABLE | WIN_HIGH_DENSITY | WIN_VULKAN; +#endif if (version == 2) return CALL(void *(*)(const char *, int, int, int, int, uint32_t), CreateWindow)( - title, CENTRED, CENTRED, width, height, WIN_SHOWN2 | WIN_RESIZABLE | WIN_HIGH_DENSITY | WIN_VULKAN); + title, CENTRED, CENTRED, width, height, WIN_SHOWN2 | (uint32_t)flags); void *w = CALL(void *(*)(const char *, int, int, uint64_t), CreateWindow)(title, width, height, - WIN_RESIZABLE | WIN_HIGH_DENSITY | WIN_VULKAN); + flags); if (w) CALL(_Bool (*)(void *, int, int), SetWindowPosition)(w, CENTRED, CENTRED); return w; } @@ -170,6 +184,10 @@ void hos_sdl_pixel_size(void *window, int *width, int *height) { if (version == 3) CALL(_Bool (*)(void *, int *, int *), PixelSize)(window, width, height); else CALL(void (*)(void *, int *, int *), PixelSize)(window, width, height); } +#ifdef __APPLE__ +void *hos_sdl_metal_create_view(void *window) { return CALL(void *(*)(void *), Metal_CreateView)(window); } +void *hos_sdl_metal_get_layer(void *view) { return CALL(void *(*)(void *), Metal_GetLayer)(view); } +#endif const uint8_t *hos_sdl_keys(int *count) { /* (SDL3's bool and SDL2's Uint8: a byte each) */ return CALL(const uint8_t *(*)(int *), GetKeyboardState)(count); } diff --git a/host/hos_sdl.h b/host/hos_sdl.h index 20e82e02e..e6018a701 100644 --- a/host/hos_sdl.h +++ b/host/hos_sdl.h @@ -23,6 +23,10 @@ void hos_sdl_relative_mouse(void *window, int on); int hos_sdl_desktop_size(int *width, int *height); int hos_sdl_vulkan_surface(void *window, void *instance, void *surface_out); /* (VkInstance, VkSurfaceKHR *): 1 if made */ void hos_sdl_pixel_size(void *window, int *width, int *height); +#ifdef __APPLE__ +void *hos_sdl_metal_create_view(void *window); +void *hos_sdl_metal_get_layer(void *view); +#endif const uint8_t *hos_sdl_keys(int *count); /* the keyboard by scancode (USB HID usage), 1 a key down */ uint32_t hos_sdl_mouse_buttons(void); /* 1 left, 2 middle, 4 right */ void hos_sdl_window_size(void *window, int *width, int *height); /* (in the units of the mouse's position, not pixels) */ diff --git a/host/hos_setjmp.c b/host/hos_setjmp.c index ec01bff44..2316aab55 100644 --- a/host/hos_setjmp.c +++ b/host/hos_setjmp.c @@ -19,16 +19,15 @@ recomp_sj_longjmp brings the host back to that caller, which runs the owner's code from the return address: what the owner would have done, ending in its own return to the caller. - (recomp_sj_setjmp / recomp_sj_longjmp are this file's own, in assembly: every Win64 callee-saved register, xmm6-15 - with them, and the stack pointer and return address. clang's __builtin_setjmp, which this used first, saves the - frame address, which on Win64 sits 128 bytes below the frame pointer clang addresses the frame's spill slots with, - and its longjmp set rbp to it: the caller's reloads of c and the slot read garbage, and the text box thread - called a stack address after its first longjmp.) */ + (recomp_sj_setjmp / recomp_sj_longjmp are this file's own, in assembly: each host ABI's callee-saved state, stack + pointer and return address. clang's __builtin_setjmp, which this used first, saves the frame address, which on + Win64 sits 128 bytes below the frame pointer clang addresses the frame's spill slots with, and its longjmp set + rbp to it: the caller's reloads of c and the slot read garbage, and the text box thread called a stack address + after its first longjmp.) */ #include "hos.h" #include #include #include -#include #include #define DEPTH 256 @@ -100,6 +99,54 @@ __attribute__((naked, noreturn)) static void recomp_sj_longjmp(void **buf, int v "jmpq *72(%rcx)\n\t"); } +#elif defined(__aarch64__) || defined(__arm64__) +/* AAPCS64: x19-x29, SP, LR and d8-d15 are callee-saved. FPCR/FPSR keep the host FP environment intact. */ +__attribute__((naked, returns_twice)) int recomp_sj_setjmp(void **buf) { + __asm__ volatile( + "stp x19, x20, [x0, #0]\n\t" + "stp x21, x22, [x0, #16]\n\t" + "stp x23, x24, [x0, #32]\n\t" + "stp x25, x26, [x0, #48]\n\t" + "stp x27, x28, [x0, #64]\n\t" + "stp x29, x30, [x0, #80]\n\t" + "mov x9, sp\n\t" + "str x9, [x0, #96]\n\t" + "stp d8, d9, [x0, #104]\n\t" + "stp d10, d11, [x0, #120]\n\t" + "stp d12, d13, [x0, #136]\n\t" + "stp d14, d15, [x0, #152]\n\t" + "mrs x9, fpcr\n\t" + "str x9, [x0, #168]\n\t" + "mrs x9, fpsr\n\t" + "str x9, [x0, #176]\n\t" + "mov w0, #0\n\t" + "ret\n\t"); +} + +__attribute__((naked, noreturn)) static void recomp_sj_longjmp(void **buf, int value) { + __asm__ volatile( + "ldp x19, x20, [x0, #0]\n\t" + "ldp x21, x22, [x0, #16]\n\t" + "ldp x23, x24, [x0, #32]\n\t" + "ldp x25, x26, [x0, #48]\n\t" + "ldp x27, x28, [x0, #64]\n\t" + "ldp x29, x30, [x0, #80]\n\t" + "ldp d8, d9, [x0, #104]\n\t" + "ldp d10, d11, [x0, #120]\n\t" + "ldp d12, d13, [x0, #136]\n\t" + "ldp d14, d15, [x0, #152]\n\t" + "ldr x9, [x0, #168]\n\t" + "msr fpcr, x9\n\t" + "ldr x9, [x0, #176]\n\t" + "msr fpsr, x9\n\t" + "ldr x9, [x0, #96]\n\t" + "mov sp, x9\n\t" + "cmp w1, #0\n\t" + "mov w0, #1\n\t" + "csel w0, w1, w0, ne\n\t" + "br x30\n\t"); +} + #else /* (linux) the System V ABI's: the buffer as above, its callee-saved registers only (rbx rbp r12-r15: rdi, rsi and the xmm registers are the caller's to keep there), buf in rdi and the value in esi */ @@ -169,8 +216,8 @@ void hos_setjmp_return(Ctx *c, uint64_t target) { for (int i = stack->depth - 1; i >= 0; i--) { SjSlot *s = &stack->slot[i]; if (s->rets[0] != target && s->rets[1] != target && s->rets[2] != target && s->rets[3] != target) continue; - static volatile LONG logged; - if (InterlockedIncrement(&logged) <= 4) + static uint32_t logged; + if (__atomic_fetch_add(&logged, 1, __ATOMIC_RELAXED) < 4) hos_log("longjmp to %#llx (value %llu, %d slots above dropped)", (unsigned long long)target, (unsigned long long)c->x[0], stack->depth - 1 - i); s->target = target; diff --git a/host/hos_thread.c b/host/hos_thread.c index ab5c71ecc..4d277c86d 100644 --- a/host/hos_thread.c +++ b/host/hos_thread.c @@ -12,7 +12,6 @@ #ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION /* (older Windows SDKs lack it: Windows 10 1803 and later take it) */ #define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002 #endif -#include #ifdef __EMSCRIPTEN__ #include #include @@ -815,7 +814,7 @@ static int present_poll(Ctx *c, int64_t ns) { const int64_t end = hos_now_ns() + ns; while (v != 1 && hos_now_ns() < end) { SwitchToThread(); - for (int i = 0; i < 64; i++) _mm_pause(); + for (int i = 0; i < 64; i++) YieldProcessor(); hos_nvn_event_value(ev, &v); } if (away) hos_vcore_back(); diff --git a/host/nn_video.c b/host/nn_video.c index 14e5233ba..e4d310d36 100644 --- a/host/nn_video.c +++ b/host/nn_video.c @@ -301,6 +301,8 @@ static void dec_finalize(Ctx *c) { free(d); set_host(ARG(0), DEC_MAGIC, NULL); } +/* (macOS port) HOS_VIDEO_TIMES=1: the decoder's time, every 5 s */ +static volatile LONG64 vt_decode_ns, vt_convert_ns, vt_pictures, vt_since; /* a decoded frame into the queue, as RGBA and NV12 (the oldest dropped when the reader falls behind) */ static void queue_frame(Dec *d, const AVFrame *f) { const int w = f->width, h = f->height; @@ -310,9 +312,16 @@ static void queue_frame(Dec *d, const AVFrame *f) { d->sws = sws_getContext(w, h, (enum AVPixelFormat)f->format, w, h, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL); d->sws_w = w, d->sws_h = h, d->sws_fmt = f->format; } + const int64_t t0 = hos_now_ns(); Picture p = {0}; p.w = w, p.h = h, p.pts = f->pts; - const int nv12 = f->format == AV_PIX_FMT_NV12; /* (hookshot: Media Foundation's pictures, kept as NV12 alone) */ + /* (hookshot: Media Foundation's pictures, kept as NV12 alone. macOS port: dav1d's planar 4:2:0 ones too, their RGBA + made by the pool only if a reader asks (pool_rgba): a 1080p RGBA conversion a picture was the decoder thread's + time for nothing, the game copying the NV12 planes. HOS_VIDEO_RGBA=1: converted as before) */ + static int rgba_always = -1; + if (rgba_always < 0) rgba_always = getenv("HOS_VIDEO_RGBA") != NULL; + const int planar = f->format == AV_PIX_FMT_YUV420P || f->format == AV_PIX_FMT_YUVA420P; + const int nv12 = f->format == AV_PIX_FMT_NV12 || (planar && !rgba_always); p.rgba = nv12 ? NULL : (uint8_t *)malloc((size_t)w * h * 4); p.nv12 = nv12 ? (uint8_t *)malloc((size_t)w * h * 3 / 2) : (uint8_t *)calloc(1, (size_t)w * h * 3 / 2); if (!p.nv12 || (!nv12 && !p.rgba)) { /* (out of memory: this frame is dropped, not written through NULL; issue 342) */ @@ -324,11 +333,11 @@ static void queue_frame(Dec *d, const AVFrame *f) { uint8_t *dst[4] = {p.rgba, NULL, NULL, NULL}; int dst_stride[4] = {w * 4, 0, 0, 0}; if (d->sws && p.rgba) sws_scale(d->sws, (const uint8_t *const *)f->data, f->linesize, 0, h, dst, dst_stride); - if (nv12) { + if (f->format == AV_PIX_FMT_NV12) { for (int y = 0; y < h; y++) memcpy(p.nv12 + (size_t)y * w, f->data[0] + (size_t)y * f->linesize[0], (size_t)w); for (int y = 0; y < h / 2; y++) memcpy(p.nv12 + (size_t)w * h + (size_t)y * w, f->data[1] + (size_t)y * f->linesize[1], (size_t)w); - } else if (f->format == AV_PIX_FMT_YUV420P || f->format == AV_PIX_FMT_YUVA420P) { /* (planar 4:2:0 -> NV12) */ + } else if (planar) { /* (planar 4:2:0 -> NV12) */ for (int y = 0; y < h; y++) memcpy(p.nv12 + (size_t)y * w, f->data[0] + (size_t)y * f->linesize[0], (size_t)w); uint8_t *uv = p.nv12 + (size_t)w * h; for (int y = 0; y < h / 2; y++) @@ -343,6 +352,8 @@ static void queue_frame(Dec *d, const AVFrame *f) { } d->queue[(d->qhead + d->qn) % MAX_QUEUE] = p; d->qn++; + InterlockedAdd64(&vt_convert_ns, hos_now_ns() - t0); + InterlockedIncrement64(&vt_pictures); } static void mf_picture(void *user, const uint8_t *y, const uint8_t *uv, int w, int h, int stride, int64_t pts) { AVFrame f; @@ -363,8 +374,29 @@ static void drain(Dec *d) { Foundation's decoder holds nothing back, so waiting for an empty queue gave the flag only to a buffer handed out again, with an older picture's pts, after the real last one went out unmarked */ static void decode_next(Dec *d); +/* (HOS_VIDEO_TIMES, above) */ +static void video_times(void) { + static int on = -1; + if (on < 0) on = getenv("HOS_VIDEO_TIMES") != NULL; + if (!on) return; + const int64_t now = hos_now_ns(), since = InterlockedCompareExchange64(&vt_since, 0, 0); + if (!since) { InterlockedExchange64(&vt_since, now); return; } + if (now - since < 5000000000LL || InterlockedCompareExchange64(&vt_since, now, since) != since) return; + const int64_t pics = InterlockedExchange64(&vt_pictures, 0), dec = InterlockedExchange64(&vt_decode_ns, 0); + const int64_t conv = InterlockedExchange64(&vt_convert_ns, 0); + hos_log("video: %lld pictures in %.1f s: decoding %.1f ms a picture (of which converting %.1f ms)", (long long)pics, + (now - since) / 1e9, pics ? dec / 1e6 / pics : 0.0, pics ? conv / 1e6 / pics : 0.0); +} static void decode_pending(Dec *d) { + if (!d->phead || (d->qn && d->phead->pkt)) return; + /* (macOS port) host work, as mkw-recomp's file calls (nn_fs.c): the virtual core goes to another guest thread while + dav1d decodes. Held through it, the movie's other threads waited up to 250 ms for a core (the opening at 12-15 fps) */ + const int64_t t0 = hos_now_ns(); + const int away = hos_vcore_away(); while (d->phead && (!d->qn || !d->phead->pkt)) decode_next(d); + if (away) hos_vcore_back(); + InterlockedAdd64(&vt_decode_ns, hos_now_ns() - t0); + video_times(); } /* one submission decoded (its pictures into the queue, if any) */ static void decode_next(Dec *d) { diff --git a/host/posix/display_sdl.c b/host/posix/display_sdl.c index fe4e3a2f0..436a9e805 100644 --- a/host/posix/display_sdl.c +++ b/host/posix/display_sdl.c @@ -11,6 +11,9 @@ the game gets none of it; the capture it had comes back when the menu closes. */ #include "../hos.h" #include "../hos_sdl.h" +#ifdef __APPLE__ +#include +#endif /* (the window's app id: the applications menu's entry names it as its StartupWMClass, builder/linux/hookshot_builder.py) */ #define HOS_APP_ID "io.github.NotRexed.Hookshot" @@ -18,7 +21,7 @@ #include #include #include -#include "../../gpu/vk/vk_api.h" +#include "../../gpu/vk/vk_api.h" /* (macOS too: MkwVkPointer and HOS_MOUSE_*, for the free mouse; C types only) */ int rx_start(void *hwnd); void rx_resize(int width, int height); @@ -97,13 +100,29 @@ static void keys_update(void) { } /* ---- the window ---- */ +#ifdef __APPLE__ +static void *metal_view, *metal_layer; +#endif static volatile int renderer; /* the renderer presents into the window */ static HANDLE renderer_up; /* (set once rx_start has returned, ready or not: hos_renderer_wait) */ static int fullscreen; +/* (macOS port) the WebGPU renderer's picture hotkeys (gpu/render/rx_present_fx.cpp: F7 frame interpolation, F8 FSR, F9 + sharpness, F10 fit / stretch), weak: a build without that renderer has none */ +extern const char *rx_present_hotkey(int key) __attribute__((weak)); +#ifndef __APPLE__ static int create_surface(void *w, void *instance, void *surface) { return hos_sdl_vulkan_surface(w, instance, surface); } static void drawable_size(void *w, int *width, int *height) { hos_sdl_pixel_size(w, width, height); } static MkwVkWindow vk_window = {MKWVK_WINDOW_VERSION, create_surface, drawable_size, NULL}; +#endif +static void window_pixel_size(void *w, int *width, int *height) { + hos_sdl_pixel_size(w, width, height); +} +#ifdef __APPLE__ +void hos_posix_window_pixel_size(int *width, int *height) { + window_pixel_size(window, width, height); +} +#endif static void mouse_capture(int on) { if (on == (int)mouse_captured) return; @@ -121,13 +140,17 @@ static void set_fullscreen(int on) { } static DWORD WINAPI renderer_main(void *p) { +#ifdef __APPLE__ + const int ok = p ? rx_start(p) : 0; +#else (void)p; const int ok = rx_start(&vk_window); +#endif if (!ok) hos_log("display: the renderer did not start: the window stays black"); renderer = ok; if (ok) { /* (a size change while it started) */ int w = 0, h = 0; - hos_sdl_pixel_size(window, &w, &h); + window_pixel_size(window, &w, &h); if (w > 0 && h > 0) rx_resize(w, h); } SetEvent(renderer_up); @@ -182,10 +205,12 @@ int hos_display_init(void) { /* (bundled SDL tries x11 before wayland. NVIDIA's X11 surface has no mailbox, and an immediate present then drops vblank for every output. Wayland has mailbox. SDL_VIDEODRIVER (SDL3: SDL_VIDEO_DRIVER) still wins. Under gamescope (XDG_CURRENT_DESKTOP names it: Steam Deck game mode) SDL keeps its own order.) */ +#ifndef __APPLE__ /* (macOS: SDL's cocoa driver, the only one there) */ const char* desktop = getenv("XDG_CURRENT_DESKTOP"); const int gamescope = desktop && strstr(desktop, "gamescope"); if (!getenv("SDL_VIDEODRIVER") && !getenv("SDL_VIDEO_DRIVER") && !gamescope) hos_sdl_hint(hos_sdl_load() == 3 ? "SDL_VIDEO_DRIVER" : "SDL_VIDEODRIVER", "wayland,x11"); +#endif if (!hos_sdl_init(HOS_SDL_VIDEO | HOS_SDL_EVENTS)) { hos_log("display: SDL_Init: %s: no window", hos_sdl_error()); return 0; @@ -226,7 +251,9 @@ void hos_display_loop(void) { SetEvent(renderer_up); for (;;) Sleep(1000); } +#ifndef __APPLE__ vk_window.window = window; +#endif hos_posix_window[0] = 1; { /* the window's icon: hookshot.png beside the game (make_game.py puts it there), for X11 and the compositors that take one from the window; the others find it through the menu entry */ @@ -237,7 +264,14 @@ void hos_display_loop(void) { hos_sdl_icon(window, png); } if (getenv("HOS_FULLSCREEN") && atoi(getenv("HOS_FULLSCREEN"))) set_fullscreen(1); +#ifdef __APPLE__ + metal_view = hos_sdl_metal_create_view(window); + metal_layer = metal_view ? hos_sdl_metal_get_layer(metal_view) : NULL; + if (!metal_layer) hos_log("display: SDL_Metal_CreateView did not provide a Metal layer"); + CloseHandle(CreateThread(NULL, 0, renderer_main, metal_layer, 0, NULL)); +#else CloseHandle(CreateThread(NULL, 0, renderer_main, NULL, 0, NULL)); +#endif for (;;) { hos_sdl_pads_pump(); if (hos_sdl_wait_event(&e, 4)) { @@ -251,7 +285,7 @@ void hos_display_loop(void) { case HOS_SDL_EV_RESIZED: if (renderer) { int w = 0, h = 0; - hos_sdl_pixel_size(window, &w, &h); + window_pixel_size(window, &w, &h); rx_resize(w, h); } break; @@ -274,6 +308,10 @@ void hos_display_loop(void) { const int scancode = e.scancode, mod = e.mod; if (e.scancode == 41) mouse_capture(0); /* Esc */ if (!e.repeat && (scancode == 68 || (scancode == 40 && (mod & 0x300)))) set_fullscreen(!fullscreen); /* F11, Alt+Enter */ + if (!e.repeat && scancode >= 64 && scancode <= 67 && rx_present_hotkey) { /* F7..F10 (SDL scancodes 64..67) */ + const char *what = rx_present_hotkey(scancode - 57); + if (what && *what) hos_log("display: %s", what); + } break; } case HOS_SDL_EV_MOUSE_DOWN: diff --git a/host/posix/macos_stubs.c b/host/posix/macos_stubs.c new file mode 100644 index 000000000..e9fb5439f --- /dev/null +++ b/host/posix/macos_stubs.c @@ -0,0 +1,13 @@ +#include +#include + +void hos_prof_start(void) {} + +typedef void (*mf_av1_picture_fn)(void *user, const uint8_t *y, const uint8_t *uv, int w, int h, int stride, int64_t pts); + +void *mf_av1_open(int w, int h) { (void)w, (void)h; return NULL; } +void mf_av1_decode(void *m, const uint8_t *data, size_t n, int64_t pts, mf_av1_picture_fn fn, void *user) { + (void)m, (void)data, (void)n, (void)pts, (void)fn, (void)user; +} +void mf_av1_flush(void *m) { (void)m; } +void mf_av1_close(void *m) { (void)m; } diff --git a/host/posix/win32_posix.c b/host/posix/win32_posix.c index 94191cc94..e680e25e7 100644 --- a/host/posix/win32_posix.c +++ b/host/posix/win32_posix.c @@ -1,6 +1,10 @@ -/* (linux) The Win32 calls of host/posix/windows.h, on POSIX. Built into the executable and, on its own, into +/* The Win32 calls of host/posix/windows.h, on POSIX. Built into the executable and, on its own, into hookshot_vk.so (each its own copy, -Bsymbolic: the executable exports nothing of it). */ #define _GNU_SOURCE +#ifdef __APPLE__ +#define _XOPEN_SOURCE 700 +#define _DARWIN_C_SOURCE +#endif #include #include @@ -9,10 +13,17 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#include +#include +#else #include +#include +#endif #include #include -#include #include #include @@ -50,9 +61,22 @@ static int64_t now_ns(void) { clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } +static uint64_t current_tid(void) { +#ifdef __APPLE__ + uint64_t tid = 0; + pthread_threadid_np(NULL, &tid); + return tid; +#else + return (uint64_t)syscall(SYS_gettid); +#endif +} static struct timespec abs_after_ms(DWORD ms) { struct timespec ts; +#ifdef __APPLE__ + clock_gettime(CLOCK_REALTIME, &ts); +#else clock_gettime(CLOCK_MONOTONIC, &ts); +#endif ts.tv_sec += ms / 1000; ts.tv_nsec += (long)(ms % 1000) * 1000000L; if (ts.tv_nsec >= 1000000000L) ts.tv_sec++, ts.tv_nsec -= 1000000000L; @@ -61,7 +85,9 @@ static struct timespec abs_after_ms(DWORD ms) { static void monitor_init(pthread_mutex_t *mu, pthread_cond_t *cv) { pthread_condattr_t a; pthread_condattr_init(&a); +#ifndef __APPLE__ pthread_condattr_setclock(&a, CLOCK_MONOTONIC); +#endif pthread_cond_init(cv, &a); pthread_condattr_destroy(&a); pthread_mutex_init(mu, NULL); @@ -77,14 +103,143 @@ static void monitor_init(pthread_mutex_t *mu, pthread_cond_t *cv) { } \ } while (0) -/* ---- futexes: condition variables, WaitOnAddress ---- */ -static long futex(volatile uint32_t *a, int op, uint32_t v, const struct timespec *ts) { - return syscall(SYS_futex, a, op, v, ts, NULL, 0); -} static struct timespec rel_ms(DWORD ms) { struct timespec ts = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000000L}; return ts; } + +/* ---- address waits: futexes on Linux, os_sync_wait_on_address on macOS (condition buckets before macOS 14.4) ---- */ +#ifdef __APPLE__ +#include +/* (macOS port) The kernel's address waits are futexes as Linux has them: a wake reaches the threads waiting on that + address and no other. The condition buckets below shared 64 condition variables among every waiter: a + WakeConditionVariable signalled one thread of its bucket, often one waiting on another address, which slept on, and + the woken one's own waiter waited for its timeout (frames of 50-250 ms in the opening; the game's worker queues + contending on the buckets' mutexes). HOS_WAIT_BUCKETS=1: the buckets, as before. */ +static int os_sync_ok(void) { + static int ok = -1; + if (ok < 0) { + ok = 0; + if (!getenv("HOS_WAIT_BUCKETS")) { + if (__builtin_available(macOS 14.4, *)) ok = 1; + } + } + return ok; +} +/* sleep while the 32-bit word at a holds v: 0 when woken (or it changed), ETIMEDOUT */ +static int os_sync_wait(volatile uint32_t *a, uint32_t v, DWORD ms) { + if (__builtin_available(macOS 14.4, *)) { + const int r = ms == INFINITE + ? os_sync_wait_on_address((void *)a, v, 4, OS_SYNC_WAIT_ON_ADDRESS_NONE) + : os_sync_wait_on_address_with_timeout((void *)a, v, 4, OS_SYNC_WAIT_ON_ADDRESS_NONE, + OS_CLOCK_MACH_ABSOLUTE_TIME, (uint64_t)(ms ? ms : 1) * 1000000ull); + if (r < 0 && errno == ETIMEDOUT) return ETIMEDOUT; + } + return 0; +} +static void os_sync_wake(volatile void *a, int all) { + if (__builtin_available(macOS 14.4, *)) { + if (all) os_sync_wake_by_address_all((void *)a, 4, OS_SYNC_WAKE_BY_ADDRESS_NONE); + else os_sync_wake_by_address_any((void *)a, 4, OS_SYNC_WAKE_BY_ADDRESS_NONE); + } +} +typedef struct { pthread_mutex_t mu; pthread_cond_t cv; } WaitBucket; +static WaitBucket wait_buckets[64]; +static pthread_once_t wait_buckets_once = PTHREAD_ONCE_INIT; +static void wait_buckets_init(void) { + for (size_t i = 0; i < sizeof wait_buckets / sizeof wait_buckets[0]; i++) { + pthread_mutex_init(&wait_buckets[i].mu, NULL); + pthread_cond_init(&wait_buckets[i].cv, NULL); + } +} +static WaitBucket *wait_bucket(const volatile void *addr) { + pthread_once(&wait_buckets_once, wait_buckets_init); + return &wait_buckets[((uintptr_t)addr >> 2) & 63]; +} +static BOOL bucket_wait(WaitBucket *b, DWORD ms, int (*changed)(void *), void *arg) { + int r = 0; + if (ms == INFINITE) { + while (!changed(arg)) pthread_cond_wait(&b->cv, &b->mu); + } else { + const struct timespec until = abs_after_ms(ms); + while (!changed(arg) && r != ETIMEDOUT) r = pthread_cond_timedwait(&b->cv, &b->mu, &until); + } + if (!changed(arg) && r == ETIMEDOUT) { last_error = ERROR_TIMEOUT; return FALSE; } + return TRUE; +} +typedef struct { CONDITION_VARIABLE *cv; uint32_t seq; } CvWait; +static int cv_changed(void *p) { + CvWait *w = (CvWait *)p; + return __atomic_load_n(&w->cv->seq, __ATOMIC_SEQ_CST) != w->seq; +} +static void wake_cv(CONDITION_VARIABLE *cv, int all) { + __atomic_add_fetch(&cv->seq, 1, __ATOMIC_SEQ_CST); + if (os_sync_ok()) { + os_sync_wake(&cv->seq, all); + return; + } + WaitBucket *b = wait_bucket(cv); + pthread_mutex_lock(&b->mu); + pthread_cond_broadcast(&b->cv); /* (a bucket's waiters wait on other addresses too: all of them look) */ + pthread_mutex_unlock(&b->mu); +} +void WakeConditionVariable(CONDITION_VARIABLE *cv) { wake_cv(cv, 0); } +void WakeAllConditionVariable(CONDITION_VARIABLE *cv) { wake_cv(cv, 1); } +static BOOL cv_wait(CONDITION_VARIABLE *cv, DWORD ms, void (*unlock)(void *), void (*relock)(void *, ULONG), void *l, ULONG flags) { + if (os_sync_ok()) { /* (as the Linux futex wait below) */ + const uint32_t seq = __atomic_load_n(&cv->seq, __ATOMIC_SEQ_CST); + unlock(l); + const int r = os_sync_wait(&cv->seq, seq, ms); + relock(l, flags); + if (r == ETIMEDOUT) { last_error = ERROR_TIMEOUT; return FALSE; } + return TRUE; + } + WaitBucket *b = wait_bucket(cv); + CvWait wait = {cv, __atomic_load_n(&cv->seq, __ATOMIC_SEQ_CST)}; + pthread_mutex_lock(&b->mu); + unlock(l); + const BOOL result = bucket_wait(b, ms, cv_changed, &wait); + pthread_mutex_unlock(&b->mu); + relock(l, flags); + return result; +} +typedef struct { volatile void *addr; void *compare; SIZE_T size; } AddressWait; +static int address_changed(void *p) { + AddressWait *w = (AddressWait *)p; + return memcmp((const void *)w->addr, w->compare, w->size) != 0; +} +BOOL WaitOnAddress(volatile void *addr, void *compare, SIZE_T size, DWORD ms) { + if (os_sync_ok()) { /* (as the Linux one: the aligned 32-bit word holding the address, woken by any change near it) */ + volatile uint32_t *w = (volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3); + const uint32_t seen = *w; + if (memcmp((const void *)addr, compare, size)) return TRUE; + if (os_sync_wait(w, seen, ms) == ETIMEDOUT) { last_error = ERROR_TIMEOUT; return FALSE; } + return TRUE; + } + WaitBucket *b = wait_bucket(addr); + AddressWait wait = {addr, compare, size}; + pthread_mutex_lock(&b->mu); + if (address_changed(&wait)) { pthread_mutex_unlock(&b->mu); return TRUE; } + const BOOL result = bucket_wait(b, ms, address_changed, &wait); + pthread_mutex_unlock(&b->mu); + return result; +} +static void wake_address(void *addr) { + if (os_sync_ok()) { + os_sync_wake((volatile void *)((uintptr_t)addr & ~(uintptr_t)3), 1); + return; + } + WaitBucket *b = wait_bucket(addr); + pthread_mutex_lock(&b->mu); + pthread_cond_broadcast(&b->cv); + pthread_mutex_unlock(&b->mu); +} +void WakeByAddressSingle(void *addr) { wake_address(addr); } +void WakeByAddressAll(void *addr) { wake_address(addr); } +#else +static long futex(volatile uint32_t *a, int op, uint32_t v, const struct timespec *ts) { + return syscall(SYS_futex, a, op, v, ts, NULL, 0); +} void WakeConditionVariable(CONDITION_VARIABLE *cv) { __atomic_add_fetch(&cv->seq, 1, __ATOMIC_SEQ_CST); futex(&cv->seq, FUTEX_WAKE_PRIVATE, 1, NULL); @@ -103,6 +258,20 @@ static BOOL cv_wait(CONDITION_VARIABLE *cv, DWORD ms, void (*unlock)(void *), vo if (timed_out) { last_error = ERROR_TIMEOUT; return FALSE; } return TRUE; } +/* (any size: the futex is the aligned 32-bit word holding the address, woken by any change near it) */ +BOOL WaitOnAddress(volatile void *addr, void *compare, SIZE_T size, DWORD ms) { + volatile uint32_t *w = (volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3); + const uint32_t seen = *w; + if (memcmp((const void *)addr, compare, size)) return TRUE; + struct timespec ts = rel_ms(ms); + const long r = futex(w, FUTEX_WAIT_PRIVATE, seen, ms == INFINITE ? NULL : &ts); + if (r == -1 && errno == ETIMEDOUT) { last_error = ERROR_TIMEOUT; return FALSE; } + return TRUE; +} +void WakeByAddressSingle(void *addr) { futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); } +void WakeByAddressAll(void *addr) { futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); } +#endif +/* (both systems: cv_wait above is the futex one on Linux, the condition buckets on macOS) */ static void srw_unlock(void *l) { pthread_rwlock_unlock((pthread_rwlock_t *)l); } static void srw_relock(void *l, ULONG flags) { if (flags & CONDITION_VARIABLE_LOCKMODE_SHARED) pthread_rwlock_rdlock((pthread_rwlock_t *)l); @@ -123,18 +292,6 @@ void InitializeCriticalSection(CRITICAL_SECTION *cs) { pthread_mutex_init(cs, &a); pthread_mutexattr_destroy(&a); } -/* (any size: the futex is the aligned 32-bit word holding the address, woken by any change near it) */ -BOOL WaitOnAddress(volatile void *addr, void *compare, SIZE_T size, DWORD ms) { - volatile uint32_t *w = (volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3); - const uint32_t seen = *w; - if (memcmp((const void *)addr, compare, size)) return TRUE; - struct timespec ts = rel_ms(ms); - const long r = futex(w, FUTEX_WAIT_PRIVATE, seen, ms == INFINITE ? NULL : &ts); - if (r == -1 && errno == ETIMEDOUT) { last_error = ERROR_TIMEOUT; return FALSE; } - return TRUE; -} -void WakeByAddressSingle(void *addr) { futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); } -void WakeByAddressAll(void *addr) { futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); } /* ---- time ---- */ void Sleep(DWORD ms) { @@ -195,7 +352,7 @@ static HThread *thread_self(void) { if (!self_thread) { /* (a thread this file did not start: the main thread, a library's) */ HThread *t = thread_new(); t->th = pthread_self(); - t->tid = (pid_t)syscall(SYS_gettid); + t->tid = (pid_t)current_tid(); t->refs = 1; /* (kept for the thread's life) */ self_thread = t; } @@ -216,10 +373,30 @@ static void thread_finish(DWORD code) { self_thread = NULL; thread_unref(t); } +#ifdef __APPLE__ +#include +/* (macOS port) the game's threads as interactive work: with the default class Apple Silicon's scheduler may run them on + the efficiency cores, where the game's six virtual cores starved its movie threads (12-15 fps in the opening). + HOS_QOS=default keeps the system's choice; =initiated, =utility for less. */ +void hos_posix_thread_qos(void) { + static int qos = -1; + if (qos < 0) { + const char *e = getenv("HOS_QOS"); + qos = !e || !strcmp(e, "interactive") ? QOS_CLASS_USER_INTERACTIVE + : !strcmp(e, "initiated") ? QOS_CLASS_USER_INITIATED + : !strcmp(e, "utility") ? QOS_CLASS_UTILITY + : QOS_CLASS_DEFAULT; + } + if (qos != QOS_CLASS_DEFAULT) pthread_set_qos_class_self_np((qos_class_t)qos, 0); +} +#endif static void *thread_entry(void *p) { HThread *t = (HThread *)p; +#ifdef __APPLE__ + hos_posix_thread_qos(); +#endif self_thread = t; - t->tid = (pid_t)syscall(SYS_gettid); + t->tid = (pid_t)current_tid(); thread_finish(t->fn(t->arg)); return NULL; } @@ -274,7 +451,11 @@ BOOL SetThreadPriority(HANDLE h, int p) { if (!t) { last_error = ERROR_INVALID_HANDLE; return FALSE; } t->priority = p; const int nice = p <= THREAD_PRIORITY_IDLE ? 19 : p <= THREAD_PRIORITY_LOWEST ? 10 : p == THREAD_PRIORITY_BELOW_NORMAL ? 5 : 0; +#ifndef __APPLE__ if (nice && t->tid) setpriority(PRIO_PROCESS, (id_t)t->tid, nice); +#else + (void)nice; +#endif return TRUE; } int GetThreadPriority(HANDLE h) { @@ -282,6 +463,11 @@ int GetThreadPriority(HANDLE h) { return t ? t->priority : 0; } DWORD_PTR SetThreadAffinityMask(HANDLE h, DWORD_PTR mask) { +#ifdef __APPLE__ + (void)h, (void)mask; + last_error = ERROR_NOT_SUPPORTED; + return 0; +#else HThread *t = thread_of(h); if (!t || !mask) { last_error = ERROR_INVALID_PARAMETER; return 0; } cpu_set_t old, want; @@ -293,8 +479,17 @@ DWORD_PTR SetThreadAffinityMask(HANDLE h, DWORD_PTR mask) { DWORD_PTR r = 0; for (int i = 0; i < 64; i++) if (CPU_ISSET(i, &old)) r |= (DWORD_PTR)1 << i; return r ? r : 1; +#endif } BOOL GetProcessAffinityMask(HANDLE p, DWORD_PTR *proc, DWORD_PTR *sys) { +#ifdef __APPLE__ + (void)p; + long n = sysconf(_SC_NPROCESSORS_ONLN); + if (n < 1) n = 1; + if (n > (long)(sizeof(DWORD_PTR) * 8)) n = sizeof(DWORD_PTR) * 8; + *proc = *sys = n == (long)(sizeof(DWORD_PTR) * 8) ? ~(DWORD_PTR)0 : (((DWORD_PTR)1 << n) - 1); + return TRUE; +#else (void)p; cpu_set_t s; CPU_ZERO(&s); @@ -305,6 +500,7 @@ BOOL GetProcessAffinityMask(HANDLE p, DWORD_PTR *proc, DWORD_PTR *sys) { *proc = m; *sys = n >= 64 ? ~(DWORD_PTR)0 : (((DWORD_PTR)1 << n) - 1); return TRUE; +#endif } int WideCharToMultiByte(UINT cp, DWORD flags, LPCWSTR w, int n, LPSTR s, int cap, LPCSTR def, BOOL *used) { (void)cp, (void)flags, (void)def; @@ -354,7 +550,11 @@ HRESULT SetThreadDescription(HANDLE h, PCWSTR name) { t->name[sizeof t->name - 1] = 0; char shortname[16]; snprintf(shortname, sizeof shortname, "%s", t->name); /* (the kernel keeps 15 characters) */ +#ifdef __APPLE__ + if (pthread_equal(pthread_self(), t->th)) pthread_setname_np(shortname); +#else pthread_setname_np(t->th, shortname); +#endif return S_OK; } HRESULT GetThreadDescription(HANDLE h, PWSTR *name) { @@ -371,6 +571,11 @@ static void filetime_ns(FILETIME *ft, int64_t ns) { ft->dwLowDateTime = (DWORD)t, ft->dwHighDateTime = (DWORD)(t >> 32); } BOOL GetThreadTimes(HANDLE h, FILETIME *create, FILETIME *exit_, FILETIME *kernel, FILETIME *user) { +#ifdef __APPLE__ + (void)h, (void)create, (void)exit_, (void)kernel, (void)user; + last_error = ERROR_NOT_SUPPORTED; + return FALSE; +#else HThread *t = thread_of(h); clockid_t clk; struct timespec ts; @@ -378,6 +583,7 @@ BOOL GetThreadTimes(HANDLE h, FILETIME *create, FILETIME *exit_, FILETIME *kerne filetime_ns(create, 0), filetime_ns(exit_, 0), filetime_ns(kernel, 0); filetime_ns(user, (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec); return TRUE; +#endif } /* (no suspending another thread: the samplers that do it find nothing to sample) */ DWORD SuspendThread(HANDLE h) { (void)h; last_error = ERROR_NOT_SUPPORTED; return (DWORD)-1; } @@ -477,8 +683,11 @@ DWORD WaitForSingleObject(HANDLE h, DWORD ms) { pthread_mutex_unlock(&t->mu); } else if (is(h, H_TIMER)) { const int64_t due = ((HTimer *)h)->due; - struct timespec ts = {(time_t)(due / 1000000000LL), (long)(due % 1000000000LL)}; - while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) == EINTR) {} + while (now_ns() < due) { + const int64_t left = due - now_ns(); + struct timespec ts = {(time_t)(left / 1000000000LL), (long)(left % 1000000000LL)}; + while (nanosleep(&ts, &ts) && errno == EINTR) {} + } r = WAIT_OBJECT_0; } else { last_error = ERROR_INVALID_HANDLE; @@ -536,7 +745,9 @@ void *CreateFiberEx(SIZE_T commit, SIZE_T reserve, DWORD flags, LPFIBER_START_RO size = (size + 0xFFFF) & ~(size_t)0xFFFF; void *stack = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_STACK, -1, 0); if (stack == MAP_FAILED) { last_error = ERROR_NOT_ENOUGH_MEMORY; return NULL; } +#ifndef __APPLE__ mprotect(stack, 4096, PROT_NONE); /* (a guard page at the bottom) */ +#endif Fiber *f = (Fiber *)calloc(1, sizeof *f); f->param = param, f->fn = fn, f->stack = stack, f->stack_size = size; getcontext(&f->uc); @@ -545,6 +756,10 @@ void *CreateFiberEx(SIZE_T commit, SIZE_T reserve, DWORD flags, LPFIBER_START_RO f->uc.uc_link = NULL; const uint64_t p = (uint64_t)(uintptr_t)f; makecontext(&f->uc, (void (*)(void))fiber_start, 2, (unsigned)p, (unsigned)(p >> 32)); +#ifdef __APPLE__ + /* (macOS's makecontext writes over the whole stack, so the guard goes on after it; pages are 16 KB on Apple Silicon) */ + mprotect(stack, (size_t)getpagesize(), PROT_NONE); +#endif return f; } void *CreateFiber(SIZE_T stack, LPFIBER_START_ROUTINE fn, void *param) { return CreateFiberEx(stack, stack, 0, fn, param); } @@ -645,7 +860,12 @@ void *VirtualAlloc(void *addr, SIZE_T size, DWORD type, DWORD protect) { const uintptr_t page = 4096; if (type & MEM_RESET) { /* (the contents are not wanted any more: they read as zero after this) */ const uintptr_t lo = ((uintptr_t)addr + page - 1) & ~(page - 1), hi = ((uintptr_t)addr + size) & ~(page - 1); - if (hi > lo) discard_pages((void *)lo, hi - lo); + if (hi > lo) { +#ifdef __APPLE__ + memset((void *)lo, 0, hi - lo); +#endif + discard_pages((void *)lo, hi - lo); + } return addr; } if (addr && (type & MEM_COMMIT) && !(type & MEM_RESERVE)) { @@ -671,6 +891,9 @@ void *VirtualAlloc(void *addr, SIZE_T size, DWORD type, DWORD protect) { BOOL VirtualFree(void *addr, SIZE_T size, DWORD type) { if (type & MEM_DECOMMIT) { /* (kept mapped: decommitted memory reads as zero when touched again) */ const uintptr_t lo = (uintptr_t)addr & ~(uintptr_t)4095, hi = ((uintptr_t)addr + size + 4095) & ~(uintptr_t)4095; +#ifdef __APPLE__ + if (hi > lo) memset((void *)lo, 0, hi - lo); +#endif return discard_pages((void *)lo, hi - lo) == 0 ? TRUE : fail_errno(); } if (type & MEM_RELEASE) { @@ -705,6 +928,26 @@ SIZE_T VirtualQuery(const void *addr, MEMORY_BASIC_INFORMATION *mbi, SIZE_T size return sizeof *mbi; } /* (anything else: the kernel's map of the process) */ +#ifdef __APPLE__ + mach_vm_address_t region = page; + mach_vm_size_t region_size = 0; + vm_region_basic_info_data_64_t info; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + const kern_return_t kr = mach_vm_region(mach_task_self(), ®ion, ®ion_size, VM_REGION_BASIC_INFO_64, + (vm_region_info_t)&info, &count, &object); + if (object != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), object); + if (kr != KERN_SUCCESS || page < region || page >= region + region_size) return sizeof *mbi; + mbi->AllocationBase = (void *)(uintptr_t)region; + mbi->RegionSize = (SIZE_T)(region + region_size - page); + const vm_prot_t prot = info.protection; + mbi->State = prot ? MEM_COMMIT : MEM_RESERVE; + mbi->Protect = mbi->AllocationProtect = !(prot & VM_PROT_READ) ? PAGE_NOACCESS + : prot & VM_PROT_EXECUTE ? ((prot & VM_PROT_WRITE) ? PAGE_EXECUTE_READWRITE : PAGE_EXECUTE_READ) + : (prot & VM_PROT_WRITE) ? PAGE_READWRITE : PAGE_READONLY; + mbi->Type = 0x20000; + return sizeof *mbi; +#else FILE *f = fopen("/proc/self/maps", "r"); char line[512]; uintptr_t prev_end = 0; @@ -730,18 +973,33 @@ SIZE_T VirtualQuery(const void *addr, MEMORY_BASIC_INFORMATION *mbi, SIZE_T size } if (f) fclose(f); return sizeof *mbi; +#endif } /* ---- modules ---- */ static void exe_path(char *out, size_t cap) { +#ifdef __APPLE__ + char path[PATH_MAX], resolved[PATH_MAX]; + uint32_t n = sizeof path; + if (_NSGetExecutablePath(path, &n) != 0 || !realpath(path, resolved)) { + if (cap) out[0] = 0; + return; + } + if (cap) snprintf(out, cap, "%s", resolved); +#else const ssize_t n = readlink("/proc/self/exe", out, cap - 1); out[n > 0 ? n : 0] = 0; -} -/* a Windows library name, as the Linux one: SDL3.dll -> libSDL3.so.0, SDL2.dll -> libSDL2-2.0.so.0, - vulkan-1.dll -> libvulkan.so.1, x.dll -> x.so */ -static const char *linux_name(const char *base) { +#endif +} +/* Windows library names mapped to their POSIX equivalents. */ +static const char *posix_name(const char *base) { +#ifdef __APPLE__ + static const char *map[][2] = {{"sdl3.dll", "libSDL3.dylib"}, {"sdl2.dll", "libSDL2-2.0.0.dylib"}, + {"vulkan-1.dll", "libvulkan.1.dylib"}}; +#else static const char *map[][2] = {{"sdl3.dll", "libSDL3.so.0"}, {"sdl2.dll", "libSDL2-2.0.so.0"}, {"vulkan-1.dll", "libvulkan.so.1"}}; +#endif for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) if (!strcasecmp(base, map[i][0])) return map[i][1]; return NULL; @@ -753,14 +1011,18 @@ HMODULE LoadLibraryA(LPCSTR name) { for (char *p = path; *p; p++) if (*p == '\\') *p = '/'; char *slash = strrchr(path, '/'); char *base = slash ? slash + 1 : path; - const char *mapped = linux_name(base); + const char *mapped = posix_name(base); if (mapped) { snprintf(base, sizeof path - (size_t)(base - path), "%s", mapped); } else { const size_t n = strlen(path); if (n > 4 && !strcasecmp(path + n - 4, ".dll")) { if (!slash) return NULL; /* (a Windows system library: kernel32, xinput, imm32 ...) */ +#ifdef __APPLE__ + strcpy(path + n - 4, ".dylib"); +#else strcpy(path + n - 4, ".so"); +#endif } } HMODULE m = dlopen(path, RTLD_NOW | RTLD_LOCAL); @@ -799,7 +1061,9 @@ DWORD GetModuleFileNameA(HMODULE m, LPSTR out, DWORD cap) { if (!path[0]) exe_path(path, sizeof path); } else { void *lm = NULL; /* (a handle from LoadLibraryA) */ +#ifndef __APPLE__ if (dlinfo(m, RTLD_DI_LINKMAP, &lm) == 0 && lm) snprintf(path, sizeof path, "%s", ((const char **)lm)[1]); +#endif } if (!cap) return 0; const size_t n = strlen(path); @@ -1082,20 +1346,52 @@ static struct sigaction previous[32]; static pthread_mutex_t handlers_mu = PTHREAD_MUTEX_INITIALIZER; static void context_from(CONTEXT *c, const ucontext_t *uc) { +#if defined(__APPLE__) && (defined(__aarch64__) || defined(__arm64__)) + const arm_thread_state64_t *g = &uc->uc_mcontext->__ss; + c->Rax = g->__x[0], c->Rcx = g->__x[1], c->Rdx = g->__x[2], c->Rbx = g->__x[3]; + c->Rsp = g->__sp, c->Rbp = g->__fp, c->Rsi = g->__x[4], c->Rdi = g->__x[5]; + c->R8 = g->__x[6], c->R9 = g->__x[7], c->R10 = g->__x[8], c->R11 = g->__x[9]; + c->R12 = g->__x[10], c->R13 = g->__x[11], c->R14 = g->__x[12], c->R15 = g->__x[13]; + c->Rip = g->__pc, c->EFlags = g->__cpsr, c->ContextFlags = CONTEXT_FULL; +#elif defined(__APPLE__) + const x86_thread_state64_t *g = &uc->uc_mcontext->__ss; + c->Rax = g->__rax, c->Rcx = g->__rcx, c->Rdx = g->__rdx, c->Rbx = g->__rbx; + c->Rsp = g->__rsp, c->Rbp = g->__rbp, c->Rsi = g->__rsi, c->Rdi = g->__rdi; + c->R8 = g->__r8, c->R9 = g->__r9, c->R10 = g->__r10, c->R11 = g->__r11; + c->R12 = g->__r12, c->R13 = g->__r13, c->R14 = g->__r14, c->R15 = g->__r15; + c->Rip = g->__rip, c->EFlags = g->__rflags, c->ContextFlags = CONTEXT_FULL; +#else const greg_t *g = uc->uc_mcontext.gregs; c->Rax = (DWORD64)g[REG_RAX], c->Rcx = (DWORD64)g[REG_RCX], c->Rdx = (DWORD64)g[REG_RDX], c->Rbx = (DWORD64)g[REG_RBX]; c->Rsp = (DWORD64)g[REG_RSP], c->Rbp = (DWORD64)g[REG_RBP], c->Rsi = (DWORD64)g[REG_RSI], c->Rdi = (DWORD64)g[REG_RDI]; c->R8 = (DWORD64)g[REG_R8], c->R9 = (DWORD64)g[REG_R9], c->R10 = (DWORD64)g[REG_R10], c->R11 = (DWORD64)g[REG_R11]; c->R12 = (DWORD64)g[REG_R12], c->R13 = (DWORD64)g[REG_R13], c->R14 = (DWORD64)g[REG_R14], c->R15 = (DWORD64)g[REG_R15]; c->Rip = (DWORD64)g[REG_RIP], c->EFlags = (DWORD)g[REG_EFL], c->ContextFlags = CONTEXT_FULL; +#endif } static void context_to(ucontext_t *uc, const CONTEXT *c) { +#if defined(__APPLE__) && (defined(__aarch64__) || defined(__arm64__)) + arm_thread_state64_t *g = &uc->uc_mcontext->__ss; + g->__x[0] = c->Rax, g->__x[1] = c->Rcx, g->__x[2] = c->Rdx, g->__x[3] = c->Rbx; + g->__sp = c->Rsp, g->__fp = c->Rbp, g->__x[4] = c->Rsi, g->__x[5] = c->Rdi; + g->__x[6] = c->R8, g->__x[7] = c->R9, g->__x[8] = c->R10, g->__x[9] = c->R11; + g->__x[10] = c->R12, g->__x[11] = c->R13, g->__x[12] = c->R14, g->__x[13] = c->R15; + g->__pc = c->Rip, g->__cpsr = c->EFlags; +#elif defined(__APPLE__) + x86_thread_state64_t *g = &uc->uc_mcontext->__ss; + g->__rax = c->Rax, g->__rcx = c->Rcx, g->__rdx = c->Rdx, g->__rbx = c->Rbx; + g->__rsp = c->Rsp, g->__rbp = c->Rbp, g->__rsi = c->Rsi, g->__rdi = c->Rdi; + g->__r8 = c->R8, g->__r9 = c->R9, g->__r10 = c->R10, g->__r11 = c->R11; + g->__r12 = c->R12, g->__r13 = c->R13, g->__r14 = c->R14, g->__r15 = c->R15; + g->__rip = c->Rip; +#else greg_t *g = uc->uc_mcontext.gregs; g[REG_RAX] = (greg_t)c->Rax, g[REG_RCX] = (greg_t)c->Rcx, g[REG_RDX] = (greg_t)c->Rdx, g[REG_RBX] = (greg_t)c->Rbx; g[REG_RSP] = (greg_t)c->Rsp, g[REG_RBP] = (greg_t)c->Rbp, g[REG_RSI] = (greg_t)c->Rsi, g[REG_RDI] = (greg_t)c->Rdi; g[REG_R8] = (greg_t)c->R8, g[REG_R9] = (greg_t)c->R9, g[REG_R10] = (greg_t)c->R10, g[REG_R11] = (greg_t)c->R11; g[REG_R12] = (greg_t)c->R12, g[REG_R13] = (greg_t)c->R13, g[REG_R14] = (greg_t)c->R14, g[REG_R15] = (greg_t)c->R15; g[REG_RIP] = (greg_t)c->Rip; +#endif } static void on_signal(int sig, siginfo_t *si, void *uctx) { ucontext_t *uc = (ucontext_t *)uctx; @@ -1109,7 +1405,13 @@ static void on_signal(int sig, siginfo_t *si, void *uctx) { rec.ExceptionAddress = (void *)(uintptr_t)ctx.Rip; if (sig == SIGSEGV || sig == SIGBUS) { rec.NumberParameters = 2; +#if defined(__APPLE__) && (defined(__aarch64__) || defined(__arm64__)) + rec.ExceptionInformation[0] = (uc->uc_mcontext->__es.__esr & (1u << 6)) ? 1 : 0; +#elif defined(__APPLE__) + rec.ExceptionInformation[0] = (uc->uc_mcontext->__es.__err & 2) ? 1 : 0; +#else rec.ExceptionInformation[0] = (uc->uc_mcontext.gregs[REG_ERR] & 2) ? 1 : 0; /* (1: a write) */ +#endif rec.ExceptionInformation[1] = (ULONG_PTR)si->si_addr; } EXCEPTION_POINTERS ep = {&rec, &ctx}; @@ -1232,8 +1534,13 @@ int MessageBoxA(HWND w, LPCSTR text, LPCSTR caption, UINT type) { (void)w, (void)type; fprintf(stderr, "%s: %s\n", caption ? caption : "Hookshot", text ? text : ""); typedef int (*ShowFn)(uint32_t, const char *, const char *, void *); /* (SDL3's and SDL2's alike; its result unread) */ +#ifdef __APPLE__ + void *sdl = dlopen("libSDL3.dylib", RTLD_NOW | RTLD_NOLOAD); + if (!sdl) sdl = dlopen("libSDL2-2.0.0.dylib", RTLD_NOW | RTLD_NOLOAD); +#else void *sdl = dlopen("libSDL3.so.0", RTLD_NOW | RTLD_NOLOAD); if (!sdl) sdl = dlopen("libSDL2-2.0.so.0", RTLD_NOW | RTLD_NOLOAD); +#endif ShowFn show = sdl ? (ShowFn)dlsym(sdl, "SDL_ShowSimpleMessageBox") : NULL; if (show) show(0x10, caption ? caption : "Hookshot", text ? text : "", NULL); return 1; diff --git a/host/posix/windows.h b/host/posix/windows.h index 85d262f0f..98963e42c 100644 --- a/host/posix/windows.h +++ b/host/posix/windows.h @@ -1,5 +1,5 @@ -/* (linux) The part of the Win32 API Hookshot's runtime and Vulkan back end use, on POSIX: host/posix is on the include - path of Linux builds only (nx2/build.py), so `#include ` finds this file and the Windows code paths stay +/* The part of the Win32 API Hookshot's runtime and Vulkan back end use, on POSIX: host/posix is on the include + path of Linux and macOS builds (nx2/build.py), so `#include ` finds this file and the Windows code paths stay as they are. Implemented in win32_posix.c (threads, events, fibers, files, memory, modules, exceptions, waveOut), gdi_posix.c (the bit of GDI the Insert menu draws its text with) and display_sdl.c (the window, keyboard, mouse). @@ -25,6 +25,22 @@ #include #include #include +#ifndef MAP_NORESERVE +#define MAP_NORESERVE 0 +#endif +#ifndef MAP_STACK +#define MAP_STACK 0 +#endif +#ifndef MAP_FIXED_NOREPLACE +#define MAP_FIXED_NOREPLACE 0 +#endif +#ifndef MADV_DONTNEED +#ifdef MADV_FREE_REUSABLE +#define MADV_DONTNEED MADV_FREE_REUSABLE +#else +#define MADV_DONTNEED 4 +#endif +#endif #ifdef __cplusplus extern "C" { #endif @@ -148,7 +164,11 @@ void SetLastError(DWORD e); #define InterlockedCompareExchangePointer InterlockedCompareExchange #define MemoryBarrier() __atomic_thread_fence(__ATOMIC_SEQ_CST) #define _ReadWriteBarrier() __atomic_signal_fence(__ATOMIC_SEQ_CST) +#if defined(__aarch64__) || defined(__arm64__) +#define YieldProcessor() __builtin_arm_yield() +#else #define YieldProcessor() __builtin_ia32_pause() +#endif /* ---- locks: SRW locks are POSIX rwlocks (zero-initialised is valid, as on Windows); condition variables a futex word, so they wait with either an SRW lock or a critical section ---- */ diff --git a/host/rx_backend.c b/host/rx_backend.c index 18e8605d0..7474a082c 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -244,6 +244,9 @@ static const MkwVkRenderer *load_vulkan(const char *dll) { int rx_start(void *hwnd) { const char *b = getenv("RX_BACKEND"); +#ifdef __APPLE__ + if (b && _stricmp(b, "metal")) hos_log("rx: RX_BACKEND=%s is not supported on macOS; using Metal via WebGPU", b); +#else const int eden = b && !_stricmp(b, "eden"); if (!b || !_stricmp(b, "vulkan") || eden) { /* (hookshot: Vulkan unless told otherwise; a started game has no RX_BACKEND) */ const char *dll = eden ? "mkw_eden.dll" : "hookshot_vk.dll"; @@ -256,6 +259,7 @@ int rx_start(void *hwnd) { } if (r) hos_log("rx: the Vulkan back end could not start on the window: WebGPU"); } +#endif const int ok = wgr_start(hwnd); flush_early(); return ok; @@ -387,7 +391,22 @@ int rx_guest_imported(uint64_t lo, uint64_t hi) { return r; } /* (nvn_native.c: the CPU set an event's value) in order with the submits, for the GPU's waits on it */ -int rx_option(int id, int otherwise) { return vk && vk->option ? vk->option(id) : otherwise; } +int rx_option(int id, int otherwise) { + if (vk && vk->option) return vk->option(id); + /* (macOS port) no Insert menu with the WebGPU renderer: the menu's graphics and gameplay choices from the environment + (hookshot_env.txt beside the game; HOS_QUALITY, hos_main.c, sets them as a preset). 0: real-time game speed + (HOS_REALTIME_STEP, on); 2: the game's dynamic resolution (HOS_DYNRES, on unless HOS_RES_SCALE fixes a scale); + 3: the fixed resolution's percent (HOS_RES_SCALE as a fraction) */ + const char *e; + switch (id) { + case 0: return (e = getenv("HOS_REALTIME_STEP")) ? atoi(e) != 0 : otherwise; + case 2: + if ((e = getenv("HOS_DYNRES"))) return atoi(e) != 0; + return getenv("HOS_RES_SCALE") ? 0 : otherwise; + case 3: return (e = getenv("HOS_RES_SCALE")) ? (int)(atof(e) * 100 + 0.5) : otherwise; + default: return otherwise; + } +} void rx_option_set(int id, int value) { if (vk && vk->option_set) vk->option_set(id, value); } void rx_nvn_event_signal(uint64_t gpu, uint32_t value) { if (vk && vk->nvn_event_signal) vk->nvn_event_signal(gpu, value); diff --git a/nx2/build.py b/nx2/build.py index a350b25f6..2cedf836b 100644 --- a/nx2/build.py +++ b/nx2/build.py @@ -49,20 +49,23 @@ PGO = os.environ.get("RX_PGO", "") -# (linux) Linux builds: an ELF executable and hookshot_vk.so, the Win32 calls of host/ and gpu/vk on POSIX -# (host/posix: its windows.h is found first), the window and sound on SDL2. HOOKSHOT_SYSROOT=: compile and link -# against that root (the AppImage's: glibc 2.35, so the game runs on older systems than the one that built it). -# HOOKSHOT_VK_PREBUILT=: the Vulkan back end as built already (the AppImage's), not compiled here. -LINUX = not toolchain.windows() # (hookshot: RX_TARGET=windows on Linux builds the Windows game, with llvm-mingw) +WINDOWS = sys.platform == "win32" +LINUX = sys.platform.startswith("linux") +MACOS = sys.platform == "darwin" +POSIX_HOST = LINUX or MACOS +# Linux uses Vulkan. macOS builds native arm64 and uses WebGPU's Metal backend. POSIX = os.path.join(kit.HERE, "..", "host", "posix") SYSROOT = os.environ.get("HOOKSHOT_SYSROOT", "") -EXE = "hookshot" if LINUX else "hookshot.exe" +EXE = "hookshot" if POSIX_HOST else "hookshot.exe" VK_LIB = "hookshot_vk.so" if LINUX else "hookshot_vk.dll" def target(): """toolchain.native_target(), with the sysroot on Linux""" - return toolchain.native_target() + (["--sysroot=" + SYSROOT] if LINUX and SYSROOT else []) + target = toolchain.macos_target_flags() if MACOS else toolchain.native_target() + if MACOS: + target += toolchain.macos_sysroot_flags() + return target + (["--sysroot=" + SYSROOT] if LINUX and SYSROOT else []) def no_throttle(handle=None): @@ -88,18 +91,20 @@ def HOT_ORDER(): return [] if LINUX: return ["-Wl,--symbol-ordering-file=" + p, "-Wl,--no-warn-symbol-ordering"] + if MACOS: + return [] return [toolchain.link_opt("/order:@" + p.replace("\\", "/")), toolchain.link_opt("/ignore:4037")] -HOST_LIBS = ["-lpthread", "-ldl", "-lm"] if LINUX else \ - ["-lwinhttp", "-luser32", "-lwinmm", "-ladvapi32", "-lole32", "-lshell32", "-lgdi32", "-ldbghelp"] +HOST_LIBS = (["-lpthread", "-ldl", "-lm"] if LINUX else ["-pthread", "-lm", "-lc++"] if MACOS else + ["-lwinhttp", "-luser32", "-lwinmm", "-ladvapi32", "-lole32", "-lshell32", "-lgdi32", "-ldbghelp"]) GPU = os.path.join(kit.HERE, "..", "gpu") # FFmpeg (libavcodec's VP8/VP9 decoders, swscale) for nn::codec's video decoders (host/nn_video.c): the static build # in the Eglantine clone's package cache FFMPEG = None # (found in the CPM caches below, once cpm_dirs is defined) # the renderer's WebGPU: a wgpu-native release (include/, lib/); TOTKPort's build of it by default -WGPU = os.environ.get("WGPU_NATIVE", os.path.join(os.path.expanduser("~"), "Desktop", "TOTKPort", "third_party", - "wgpu-native")) +WGPU = os.environ.get("WGPU_NATIVE", os.path.join(os.path.expanduser("~"), "wgpu-native") if MACOS else + os.path.join(os.path.expanduser("~"), "Desktop", "TOTKPort", "third_party", "wgpu-native")) def gpu_sources(): @@ -335,7 +340,7 @@ def ffmpeg_linux(): return None -if LINUX: +if POSIX_HOST: FFMPEG_LX = ffmpeg_linux() FFMPEG = None @@ -346,7 +351,7 @@ def ffmpeg_version_key(): incs = [] if FFMPEG: incs.append(os.path.join(FFMPEG, "include")) - if LINUX and FFMPEG_LX: + if POSIX_HOST and FFMPEG_LX: incs += [f[2:] for f in FFMPEG_LX[0] if f.startswith("-I")] incs.append("/usr/include") for inc in incs: @@ -373,16 +378,18 @@ def ffmpeg_version_key(): # 0xc000001d. The game is built on the PC it runs on: a CPU without x86-64-v3's features gets x86-64-v2, or x86-64) from host_cpu import host_march # (make_game.py asks it too: the AppImage's renderer) -MARCH = os.environ.get("RX_MARCH") or host_march() -if MARCH != "x86-64-v3": +MARCH = os.environ.get("RX_MARCH") or ("apple-m1" if MACOS else host_march()) +if not MACOS and MARCH != "x86-64-v3": print("build: this CPU has no AVX2: the game is built for %s (a little slower)" % MARCH, flush=True) -CPU = ["-march=" + MARCH, "-ffp-contract=off"] +CPU = [("-mcpu=" if MACOS else "-march=") + MARCH, "-ffp-contract=off"] +ATOMIC = [] if MACOS else ["-mcx16"] def flags(gen, rt64, opt, polls): return target() + CPU + [ "-I" + rt64, "-I" + gen, "-I" + os.path.join(kit.KIT, "rt"), "-I" + NX2_RT, "-Wno-everything", - "-fno-strict-aliasing", "-mcx16", "-" + opt, "-g0", "-mllvm", "-rotation-max-header-size=0", + "-fno-strict-aliasing"] + ATOMIC + [ + "-" + opt, "-g0", "-mllvm", "-rotation-max-header-size=0", "-ffunction-sections", "-mllvm", "-capture-tracking-max-uses-to-explore=5000"] + \ (["-DA64_POLLING"] if polls else []) + WRITE_TRACK @@ -610,9 +617,11 @@ def main(): # its own stand-ins, host/posix/linux_stubs.c) have_wgpu = os.path.isfile(os.path.join(WGPU, "include", "webgpu", "webgpu.h")) host_srcs = sorted(os.path.join(HOST, f) for f in os.listdir(HOST) if f.endswith(".c") and (f != "pgo_rt.c" or PGO == "gen") - and not (LINUX and f in ("display.c", "mf_av1.c", "rx_nowgpu.c")) and (f != "rx_nowgpu.c" or not have_wgpu)) + and not (POSIX_HOST and f in ("display.c", "mf_av1.c", "rx_nowgpu.c")) and (f != "rx_nowgpu.c" or not have_wgpu)) if LINUX: # (the window on SDL2, the Win32 calls on POSIX, no Media Foundation and no WebGPU renderer) host_srcs += [os.path.join(POSIX, f) for f in ("win32_posix.c", "display_sdl.c", "linux_stubs.c", "prof.c")] + elif MACOS: + host_srcs += [os.path.join(POSIX, f) for f in ("win32_posix.c", "display_sdl.c", "macos_stubs.c")] else: srcs += [os.path.join(NX2_RT, "eden_shim.c"), write_info(out)] if os.environ.get("RX_VK_ONLY"): # (no lift needed: the back end alone) @@ -646,18 +655,20 @@ def main(): fp(deps), fp(norm(fl)), fp(norm(hot_fl)), code.hexdigest()[:10], sum(1 for x in srcs if os.path.basename(x).startswith("code_"))), flush=True) # the runtime: ordinary C at -O2, with its own headers (any host/*.h change rebuilds all of it) - host_fl = target() + CPU + (["-I" + POSIX, "-D_GNU_SOURCE"] if LINUX else []) + \ - ["-I" + HOST, "-I" + rt64, "-I" + gen, "-Wno-everything", "-O2", "-g0", "-mcx16", "-DA64_POLLING"] + WRITE_TRACK + host_fl = target() + CPU + (["-I" + POSIX, "-D_GNU_SOURCE"] if POSIX_HOST else []) + \ + ["-I" + HOST, "-I" + rt64, "-I" + gen, "-Wno-everything", "-O2", "-g0"] + ATOMIC + \ + ["-DA64_POLLING"] + WRITE_TRACK host_deps = deps + b"".join(open(os.path.join(HOST, f), "rb").read() for f in sorted(os.listdir(HOST)) if f.endswith(".h")) - if LINUX: + if POSIX_HOST: host_deps += b"".join(open(os.path.join(POSIX, f), "rb").read() for f in sorted(os.listdir(POSIX)) if f.endswith(".h")) + if LINUX: host_deps += open(os.path.join(GPU, "vk", "vk_api.h"), "rb").read() host_deps += ffmpeg_version_key() jobs += [(cc, s, os.path.join(obj_dir, "host_" + os.path.basename(s)[:-2] + ".o"), host_fl + ["-I" + os.path.join(OPUS, "include")] + (["-I" + os.path.join(FFMPEG, "include")] if FFMPEG else []) + - (FFMPEG_LX[0] if LINUX and FFMPEG_LX else []) + + (FFMPEG_LX[0] if POSIX_HOST and FFMPEG_LX else []) + ([stamp_def] if os.path.basename(s) == "hos_main.c" else []), host_deps) for s in host_srcs] # libopus 1.5.2 (third_party/opus, from TOTKPort): nn::codec's hardware Opus decoder (host/nn_codec.c) if host_srcs: @@ -669,13 +680,17 @@ def main(): # the renderer (C++20, NDEBUG: no asserts in it or suyu), when a wgpu-native release is there wgpu = a.standalone and not a.only and not LINUX and os.path.isfile(os.path.join(WGPU, "include", "webgpu", "webgpu.h")) if a.standalone and not a.only and not wgpu and not LINUX: + if MACOS: + raise SystemExit("macOS requires wgpu-native with Metal support; set WGPU_NATIVE to its package directory") print("no wgpu-native at %s (WGPU_NATIVE): building without the renderer" % WGPU) if wgpu: cxx = toolchain.need(toolchain.find("clang++")) gflags = target() + ["-std=c++20", "-O2", "-g0", "-DNDEBUG", "-Wno-everything", "-I" + os.path.join(GPU, "compat"), "-I" + os.path.join(GPU, "third_party", "suyu"), - "-I" + os.path.join(WGPU, "include"), "-I" + HOST, "-I" + rt64] + WRITE_TRACK + ["-include", os.path.join(GPU, "render", "rx_wgpu_names.h")] # (rx_* -> wgr_*: host/rx_backend.c) + "-I" + os.path.join(WGPU, "include"), "-I" + HOST, "-I" + rt64] + WRITE_TRACK + \ + (["-I" + POSIX] if MACOS else []) + \ + ["-include", os.path.join(GPU, "render", "rx_wgpu_names.h")] # (rx_* -> wgr_*: host/rx_backend.c) gdeps = tree_headers(GPU, HOST, skip=("vk",)) + open(os.path.join(WGPU, "include", "webgpu", "webgpu.h"), "rb").read() for src in gpu_sources(): name = os.path.relpath(src, GPU).replace(os.sep, "_")[:-4] @@ -686,7 +701,7 @@ def main(): vk_prebuilt = os.environ.get("HOOKSHOT_VK_PREBUILT", "") if a.standalone and not a.only and vk_prebuilt: print("the Vulkan back end: %s, as built" % vk_prebuilt) - elif a.standalone and not a.only and vk_headers and spirv_headers: + elif a.standalone and not a.only and not MACOS and vk_headers and spirv_headers: cxx = toolchain.need(toolchain.find("clang++")) vk = os.path.join(GPU, "vk") # (what the shader caches are good for: what translates shaders, not the whole DLL, so a build that changes something else @@ -798,25 +813,41 @@ def main(): lib = os.path.join(out, "hookshot_game.lib") subprocess.run([toolchain.lld_link(), "/lib", "/out:" + lib, "@" + rsp], check=True) print("wrote %s (%d objects, %d MB)" % (lib, len(objs), os.path.getsize(lib) >> 20)) - if a.standalone and not a.only and not vk_only and LINUX: - exe = os.path.join(out, EXE) # (+ hookshot.map: crash offsets -> f_) + if a.standalone and not a.only and not vk_only and POSIX_HOST: + exe = os.path.join(out, EXE) t = time.time() - # (--export-dynamic-symbol: the window's state, which the Vulkan back end's own Win32 calls read by name) - r = subprocess.run([cc] + target() + ["-fuse-ld=lld", "-o", exe, "-Wl,-Map=" + exe + ".map", "-Wl,--gc-sections", - "-Wl,--export-dynamic-symbol=hos_posix_*", "@" + rsp] + HOT_ORDER() + - (["-fprofile-instr-generate"] if PGO == "gen" else []) + - (FFMPEG_LX[1] if FFMPEG_LX else []) + HOST_LIBS, - capture_output=True, text=True) + if MACOS: + wgpu_lib = os.path.join(WGPU, "lib", "libwgpu_native.dylib") + if not os.path.isfile(wgpu_lib): + raise SystemExit("macOS requires %s" % wgpu_lib) + link = [cc] + target() + ["-o", exe, "-Wl,-map," + exe + ".map", "-Wl,-dead_strip", + "-Wl,-export_dynamic", + "-Wl,-rpath,@executable_path", "@" + rsp] + HOT_ORDER() + \ + (["-fprofile-instr-generate"] if PGO == "gen" else []) + \ + (FFMPEG_LX[1] if FFMPEG_LX else []) + HOST_LIBS + [wgpu_lib, + "-framework", "Metal", "-framework", "Foundation", "-framework", "QuartzCore", "-framework", "Cocoa"] + r = subprocess.run(link, capture_output=True, text=True) + else: + # (--export-dynamic-symbol: Vulkan backend's window state on Linux) + r = subprocess.run([cc] + target() + ["-fuse-ld=lld", "-o", exe, "-Wl,-Map=" + exe + ".map", "-Wl,--gc-sections", + "-Wl,--export-dynamic-symbol=hos_posix_*", "@" + rsp] + HOT_ORDER() + + (["-fprofile-instr-generate"] if PGO == "gen" else []) + + (FFMPEG_LX[1] if FFMPEG_LX else []) + HOST_LIBS, + capture_output=True, text=True) undefined = sorted({l.strip() for l in (r.stdout + r.stderr).splitlines() if "undefined symbol" in l}) if r.returncode: print("link FAILED (%.0fs), %d undefined symbols" % (time.time() - t, len(undefined))) print("\n".join(undefined[:60]) if undefined else (r.stdout + r.stderr)[-3000:]) return 1 print("linked %s: %d MB (%.0fs)" % (exe, os.path.getsize(exe) >> 20, time.time() - t)) - sdl = os.environ.get("HOOKSHOT_SDL2", "") # (libSDL2-2.0.so.0 beside it, when the AppImage brings one) + sdl = os.environ.get("HOOKSHOT_SDL2", "") if sdl and os.path.exists(sdl): import shutil - shutil.copyfile(sdl, os.path.join(out, "libSDL2-2.0.so.0")) + name = "libSDL2-2.0.0.dylib" if MACOS else "libSDL2-2.0.so.0" + shutil.copyfile(sdl, os.path.join(out, name)) + if MACOS: # (the Metal renderer, loaded from beside the game through its @executable_path rpath) + import shutil + shutil.copyfile(os.path.join(WGPU, "lib", "libwgpu_native.dylib"), os.path.join(out, "libwgpu_native.dylib")) elif a.standalone and not a.only and not vk_only: exe = os.path.join(out, "hookshot.exe") # (+ hookshot.map: crash offsets -> f_) t = time.time() @@ -836,17 +867,15 @@ def main(): print("\n".join(undefined[:60]) if undefined else (r.stdout + r.stderr)[-3000:]) return 1 print("linked %s: %d MB (%.0fs)" % (exe, os.path.getsize(exe) >> 20, time.time() - t)) - if wgpu: # (wgpu_native.dll beside it; in use while the game runs) + if wgpu: import shutil - dll, dst = os.path.join(WGPU, "lib", "wgpu_native.dll"), os.path.join(out, "wgpu_native.dll") - if not os.path.exists(dst) or os.path.getsize(dst) != os.path.getsize(dll): - shutil.copyfile(dll, dst) - # (hookshot) a shared FFmpeg's DLLs beside it (an LGPL shared build, as cross builds find: the static one needs none) - for l in FFMPEG_LIBS if FFMPEG and os.path.isdir(os.path.join(FFMPEG, "bin")) else (): - import shutil - for f in os.listdir(os.path.join(FFMPEG, "bin")): - if f.startswith(l + "-") and f.endswith(".dll"): - shutil.copyfile(os.path.join(FFMPEG, "bin", f), os.path.join(out, f)) + if MACOS: + shutil.copyfile(os.path.join(WGPU, "lib", "libwgpu_native.dylib"), + os.path.join(out, "libwgpu_native.dylib")) + else: + dll, dst = os.path.join(WGPU, "lib", "wgpu_native.dll"), os.path.join(out, "wgpu_native.dll") + if not os.path.exists(dst) or os.path.getsize(dst) != os.path.getsize(dll): + shutil.copyfile(dll, dst) # (hookshot) SDL2.dll beside it: host/pad_sdl.c's controllers (DualSense, DualShock 4, Switch Pro ...) sdl = os.path.join(kit.HERE, "..", "third_party", "sdl2", "SDL2.dll") sdl_dst = os.path.join(out, "SDL2.dll") @@ -860,7 +889,7 @@ def main(): if os.path.exists(sdl3) and (not os.path.exists(sdl3_dst) or os.path.getsize(sdl3_dst) != os.path.getsize(sdl3)): import shutil shutil.copyfile(sdl3, sdl3_dst) - if vk_prebuilt and a.standalone and not a.only: + if vk_prebuilt and a.standalone and not a.only and not MACOS: import shutil shutil.copyfile(vk_prebuilt, os.path.join(out, VK_LIB)) if vk_jobs: # (hookshot_vk.dll beside the exe: loaded by host/rx_backend.c with RX_BACKEND=vulkan) diff --git a/nx2/make_game.py b/nx2/make_game.py index 07ca7f9d7..b51d50023 100644 --- a/nx2/make_game.py +++ b/nx2/make_game.py @@ -24,6 +24,7 @@ import hashlib import json import os +import platform import re import shutil import stat @@ -39,12 +40,15 @@ STARTED = time.time() # (the build's total time, kept in build_times.json and said as @@times) sys.path[:0] = [HERE, os.path.join(ROOT, "third_party", "a64-kit", "tools")] # (python -I leaves the script folder off) import toolchain as _toolchain -# (hookshot) RX_TARGET=windows on Linux: the Windows game, with llvm-mingw's Linux release (HOOKSHOT_MINGW, or llvm-mingw -# beside the builder), to run under Proton or copy to a Windows PC -LINUX, CROSS = not _toolchain.windows(), _toolchain.windows() and os.name != "nt" -# (SDL2.dll is sdl2-compat: it loads SDL3.dll from beside the exe. Left out of bin\ since v2.0.0, it said "Failed loading SDL3 -# library" three times and no controller worked: Discord #bug-reporting, 11-08) -BESIDE = ["hookshot", "hookshot_vk.so", "libSDL2-2.0.so.0"] if LINUX else ["hookshot.exe", "hookshot_vk.dll", "SDL2.dll", "SDL3.dll", "wgpu_native.dll"] +WINDOWS = _toolchain.windows() +MACOS = sys.platform == "darwin" and not WINDOWS +LINUX = sys.platform.startswith("linux") and not WINDOWS +CROSS = WINDOWS and os.name != "nt" +if not (WINDOWS or LINUX or MACOS): + raise SystemExit("Unsupported host platform: " + sys.platform) +BESIDE = (["hookshot", "hookshot_vk.so", "libSDL2-2.0.so.0"] if LINUX else + ["hookshot", "libSDL2-2.0.0.dylib", "libwgpu_native.dylib"] if MACOS else + ["hookshot.exe", "hookshot_vk.dll", "SDL2.dll", "SDL3.dll", "wgpu_native.dll"]) LANGUAGES = [("en-US", "English (US)"), ("en-GB", "English (UK)"), ("fr", "French (France)"), ("fr-CA", "French (Canada)"), ("de", "German"), ("it", "Italian"), ("es", "Spanish (Spain)"), ("es-419", "Spanish (Latin America)"), ("pt-BR", "Portuguese (Brazil)"), ("nl", "Dutch"), ("pl", "Polish"), ("ru", "Russian"), ("ja", "Japanese"), ("ko", "Korean"), ("zh-Hans", "Chinese (Simplified)"), @@ -84,7 +88,9 @@ def failure_reason(lines): tail, seen = [], None for line in lines: tail = (tail + [line])[-12:] - if "error:" in line.lower() and "CalledProcessError" not in line: + lower = line.lower() + if ("error:" in lower and "CalledProcessError" not in line and + "linker command failed" not in lower): seen = line if seen is not None: return seen @@ -425,6 +431,61 @@ def check_linux_compiler(): "libc6-dev on Debian and Ubuntu, glibc-devel on Fedora)" % (errors[0] if errors else "no message")) +def check_macos_toolchain(): + """Validate the native Apple Silicon compiler and graphics/audio libraries before the long lift.""" + wgpu = os.environ.get("WGPU_NATIVE", os.path.join(os.path.expanduser("~"), "wgpu-native")) + sdl = os.environ.get("HOOKSHOT_SDL2", "") + ffmpeg = os.environ.get("HOOKSHOT_FFMPEG", "") + if platform.machine() not in ("arm64", "aarch64"): + fail("The macOS builder requires a native Apple Silicon (arm64) process; Rosetta is not supported.") + if not os.path.isfile(os.path.join(wgpu, "include", "webgpu", "webgpu.h")): + fail("macOS needs arm64 wgpu-native with Metal support: set WGPU_NATIVE to its package directory.") + if not os.path.isfile(os.path.join(wgpu, "lib", "libwgpu_native.dylib")): + fail("macOS needs libwgpu_native.dylib in WGPU_NATIVE/lib.") + if not sdl or not os.path.isfile(sdl): + fail("macOS needs an arm64 SDL2 dylib: set HOOKSHOT_SDL2 to libSDL2-2.0.0.dylib.") + if not shutil.which("lipo"): + fail("Install Xcode Command Line Tools to verify macOS dependency architectures.") + libraries = [sdl, os.path.join(wgpu, "lib", "libwgpu_native.dylib")] + if ffmpeg: + headers = ("libavcodec/avcodec.h", "libswscale/swscale.h", "libswresample/swresample.h", + "libavutil/avutil.h") + missing_headers = [h for h in headers if not os.path.isfile(os.path.join(ffmpeg, "include", h))] + static_libs = ("libavcodec.a", "libswscale.a", "libswresample.a", "libavutil.a") + missing_libs = [l for l in static_libs if not os.path.isfile(os.path.join(ffmpeg, "lib", l))] + if missing_headers or missing_libs: + missing = missing_headers + missing_libs + fail("macOS needs the Hookshot static FFmpeg package; missing: %s" % ", ".join(missing)) + libraries += [os.path.join(ffmpeg, "lib", l) for l in static_libs] + for library in libraries: + r = subprocess.run(["lipo", library, "-verify_arch", "arm64"], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if r.returncode: + fail("macOS dependency must contain native arm64 code: %s" % library) + sys.path.insert(0, os.path.join(ROOT, "third_party", "a64-kit", "tools")) + import toolchain + cc = toolchain.find("clang") + if not os.path.isfile(cc): + fail("Install Xcode Command Line Tools to build Hookshot for macOS.") + with tempfile.TemporaryDirectory() as d: + src, exe = os.path.join(d, "probe.c"), os.path.join(d, "probe") + with open(src, "w", encoding="utf-8") as f: + f.write("int main(void) { return 0; }\n") + r = subprocess.run([cc] + toolchain.macos_target_flags() + toolchain.macos_sysroot_flags() + + [src, "-o", exe], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if r.returncode: + fail("Xcode Command Line Tools could not link a native arm64 macOS test program: %s" + % failure_reason((r.stdout + r.stderr).splitlines())) + if not ffmpeg: + if not shutil.which("pkg-config"): + fail("Install pkg-config and arm64 FFmpeg development libraries for the macOS build.") + r = subprocess.run(["pkg-config", "--exists", "libavcodec", "libswscale", "libswresample", "libavutil"], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if r.returncode: + fail("macOS needs arm64 FFmpeg development libraries visible through pkg-config (set PKG_CONFIG_PATH).") + + def requested_jobs(value): """The builder's explicit worker override, or None to keep its automatic selection.""" if value is None or value == "": @@ -503,13 +564,12 @@ def check_mingw(): def check_windows_sdk(): """The compiler is bundled; Microsoft's C library and Windows SDK may not be, by their licences. Found now, not 40 minutes in.""" + if MACOS: + return check_macos_toolchain() if LINUX: return check_linux_compiler() if bundled_mingw(): return check_mingw() - if CROSS: - fail("Building for Windows on Linux needs llvm-mingw: unpack its Linux release (github.com/mstorsjo/llvm-mingw) and set " - "HOOKSHOT_MINGW to its folder.") sys.path.insert(0, os.path.join(ROOT, "third_party", "a64-kit", "tools")) import toolchain cc = toolchain.find("clang") @@ -570,7 +630,7 @@ def stop_stray_compilers(work, default_work): update mid-build) keeps its files open: the next build stopped at once with 'OSError: [Errno 22]' writing sa/gen/a64_fpx.h, or WinError 1224 on the play profile. Only that build goes: an earlier make_game on this work folder, the build and lift steps whose make_game is gone, and compilers whose command line names this work folder. Never this build or what started it.""" - if LINUX: + if os.name != "nt": return ps = ("Get-CimInstance Win32_Process | Where-Object { $_.Name -like 'clang*' -or $_.Name -like 'ld.lld*' -or $_.Name -like 'python*' } " "| Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress") @@ -661,8 +721,7 @@ def drop_stale_objects(work): """(issue 231) Objects compiled against other system headers are not reused. A first build that compiled with an old Visual Studio's stdio.h (before 2015) left its objects in the cache; once the PC was fixed every later build reused them and the link failed on __iob_func. The headers the compiler finds are noted, and when they change the compiled objects go""" - if LINUX: - _drop_stale_linux(work) + if os.name != "nt": return sys.path.insert(0, os.path.join(ROOT, "third_party", "a64-kit", "tools")) import hashlib @@ -875,8 +934,9 @@ def parse_args(): p.add_argument("--romfs") p.add_argument("--exefs") p.add_argument("--out") - p.add_argument("--work", default=os.path.join(os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache"), - "hookshot-builder-windows" if CROSS else "hookshot-builder") # (the objects apart) + p.add_argument("--work", default=os.path.join(os.path.expanduser("~/Library/Caches"), "HookshotBuilder") + if MACOS else os.path.join(os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache"), + "hookshot-builder-windows" if CROSS else "hookshot-builder") if os.name != "nt" else os.path.join(os.environ.get("LOCALAPPDATA", ROOT), "HookshotBuilder")) # (HOOKSHOT_JOBS: fewer compilers on a PC short of memory; the AppImage's builds pass no --jobs) p.add_argument("--jobs", type=int) @@ -1112,6 +1172,11 @@ def compiled(line): # as it meets them: not worth stopping a finished build for if run(shader_census_cmd(romfs, os.path.join(out, "hookshot_shaders.bin")), check=False, output=sys.stderr): print("the shader list was not made: the game will compile its shaders as it plays", flush=True) + # (macOS port) the player's settings file, with the quality presets (host/hos_main.c HOS_QUALITY): there is no Insert + # menu with the Metal renderer. Only when the folder has none: the player's own choices stay + env_template = os.path.join(ROOT, "scripts", "macos", "hookshot_env.txt") + if MACOS and os.path.exists(env_template) and not os.path.exists(os.path.join(out, "hookshot_env.txt")): + shutil.copy2(env_template, os.path.join(out, "hookshot_env.txt")) icon = os.path.join(ROOT, "assets", "hookshot.png") if LINUX and os.path.exists(icon): # (the window's icon: host/posix/display_sdl.c) shutil.copy2(icon, os.path.join(out, "hookshot.png")) diff --git a/nx2/zbic.py b/nx2/zbic.py index 0179d06b3..6915223eb 100644 --- a/nx2/zbic.py +++ b/nx2/zbic.py @@ -13,7 +13,9 @@ HERE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(os.path.dirname(HERE), "third_party", "zbic") -DLL = os.path.join(SRC, "zbic.dll" if os.name == "nt" else "zbic.so") +WINDOWS = os.name == "nt" +MACOS = sys.platform == "darwin" +DLL = os.path.join(SRC, "zbic.dll" if WINDOWS else "zbic.dylib" if MACOS else "zbic.so") _lib = None @@ -24,7 +26,9 @@ def _build(): return srcs = [os.path.join(SRC, "zstd.c"), os.path.join(SRC, "zbic_api.c")] cmd = [toolchain.need(toolchain.find("clang", host=True)), *toolchain.native_target(host=True), *toolchain.native_link(host=True), - "-O2", "-shared", *([] if os.name == "nt" else ["-fPIC"]), "-DZSTD_ZBIC_SUPPORT=1", "-I", SRC, "-o", DLL, *srcs] + *(toolchain.macos_sysroot_flags() if MACOS else []), # (clang called by its full path finds no SDK headers on its own) + "-O2", *(["-dynamiclib"] if MACOS else ["-shared"]), + *([] if WINDOWS or MACOS else ["-fPIC"]), "-DZSTD_ZBIC_SUPPORT=1", "-I", SRC, "-o", DLL, *srcs] print("building", os.path.basename(DLL), file=sys.stderr) subprocess.run(cmd, check=True) diff --git a/scripts/linux/appimage.sh b/scripts/linux/appimage.sh index 2e158db09..29a039fa4 100755 --- a/scripts/linux/appimage.sh +++ b/scripts/linux/appimage.sh @@ -10,6 +10,10 @@ # game it builds runs on glibc 2.35 and newer. work/ (hot_funcs.txt, hot_order.txt, hookshot_pieces.prof, # image/chunks.txt: addresses and counts from playing, no game code) is carried along when it is there. set -euo pipefail +if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then + echo "The Linux AppImage build requires a Linux x86_64 host." >&2 + exit 1 +fi cd "$(dirname "$0")/../.." ROOT=$PWD B=$ROOT/build-linux diff --git a/scripts/linux/deps.sh b/scripts/linux/deps.sh index 194d56c37..39ae7e8eb 100755 --- a/scripts/linux/deps.sh +++ b/scripts/linux/deps.sh @@ -16,6 +16,10 @@ # runtime-x86_64 the AppImage runtime it puts in front of the image (pinned: appimagetool's own download of the # 'continuous' one, at packing time, fails whenever GitHub answers 504) set -euo pipefail +if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then + echo "Linux AppImage dependencies require a Linux x86_64 host." >&2 + exit 1 +fi cd "$(dirname "$0")/../.." DEPS=${DEPS:-$PWD/build-linux/deps} DL=$DEPS/downloads diff --git a/scripts/macos/Builder.applescript b/scripts/macos/Builder.applescript new file mode 100644 index 000000000..b43e391c9 --- /dev/null +++ b/scripts/macos/Builder.applescript @@ -0,0 +1,7 @@ +on run + set appRoot to POSIX path of (path to me) + set tools to appRoot & "Contents/Resources/tools/" + set source to tools & "source" + set commandLine to "export HOOKSHOT_TOOLS=" & quoted form of source & " && export WGPU_NATIVE=" & quoted form of (tools & "wgpu-native") & " && export HOOKSHOT_SDL2=" & quoted form of (tools & "sdl2/lib/libSDL2-2.0.0.dylib") & " && export HOOKSHOT_FFMPEG=" & quoted form of (tools & "ffmpeg") & " && export RX_BACKEND=metal && exec " & quoted form of (tools & "python/bin/python3") & " -I " & quoted form of (source & "/builder/linux/hookshot_builder.py") + do shell script commandLine +end run diff --git a/scripts/macos/README.txt b/scripts/macos/README.txt new file mode 100644 index 000000000..34179b230 --- /dev/null +++ b/scripts/macos/README.txt @@ -0,0 +1,20 @@ +Hookshot Builder for macOS (Apple Silicon) + +This package includes a native arm64 macOS app and command-line builder that use +WebGPU's Metal backend. It includes Hookshot source, Python +runtime and modules, SDL2, static FFmpeg libraries, and wgpu-native. + +It does not include game data or a built game. You must provide your own +legally obtained .nup dump. Xcode Command Line Tools are required on the Mac +where you build. +The generated executable requires macOS 13.3 or newer. + +Double-click HookshotBuilder.app to open the Hookshot Builder interface. It +matches the existing builder flow: choose your dump and output folder, then +watch the build progress. For command-line use, run: + + ./HookshotBuilder.command --nup /path/to/your-game.nup --out /path/to/output + +Add --jobs 2 (or another suitable value) to limit parallel compiler memory use. +The output folder contains the built game. Keep this builder package available +if you later use the builder's --update option. diff --git a/scripts/macos/build-game.sh b/scripts/macos/build-game.sh new file mode 100755 index 000000000..d6802a8cb --- /dev/null +++ b/scripts/macos/build-game.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Run Hookshot's builder from this checkout with the dependencies scripts/macos/deps.sh made. +# Usage: scripts/macos/build-game.sh --nup /path/to/game.nup --out /path/to/game-folder [--jobs N] [--check] +set -eu +SRC=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +DEPS="$SRC/build-macos/deps" +export WGPU_NATIVE="$DEPS/wgpu-native" +export HOOKSHOT_SDL2="$DEPS/sdl2/lib/libSDL2-2.0.0.dylib" +export HOOKSHOT_FFMPEG="$DEPS/ffmpeg" +export RX_BACKEND=metal +export SDKROOT="${SDKROOT:-$(xcrun --show-sdk-path)}" +exec "$DEPS/python/bin/python3" -I "$SRC/nx2/make_game.py" "$@" diff --git a/scripts/macos/check-arm64-translation.py b/scripts/macos/check-arm64-translation.py new file mode 100644 index 000000000..232b92f69 --- /dev/null +++ b/scripts/macos/check-arm64-translation.py @@ -0,0 +1,21 @@ +"""Verify native arm64 acquire/release forms become fenced translated operations.""" +import os +import sys + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, os.path.join(ROOT, "third_party", "a64-kit", "lift")) +import a64emit + +instructions = [ + (0x1000, 0xC85FFC20), # LDAXR X0, [X1] + (0x1004, 0xC802FC20), # STLXR W2, X0, [X1] + (0x1008, 0xC8608440), # LDAXP X0, X1, [X2] + (0x100C, 0xC82394C4), # STLXP W3, X4, X5, [X6] +] +texts, flags = a64emit.translate(instructions) +if any(flags): + raise SystemExit("arm64 acquire/release instruction was not translated") +for index, ordering in ((0, "ACQUIRE"), (1, "RELEASE"), (2, "ACQUIRE"), (3, "RELEASE")): + if "__atomic_thread_fence(__ATOMIC_%s)" % ordering not in texts[index]: + raise SystemExit("missing %s fence in translated instruction %#x" % (ordering, instructions[index][1])) +print("arm64 exclusive acquire/release instructions compile to fenced translated operations.") diff --git a/scripts/macos/check-renderer.py b/scripts/macos/check-renderer.py new file mode 100644 index 000000000..2138697d4 --- /dev/null +++ b/scripts/macos/check-renderer.py @@ -0,0 +1,41 @@ +"""Syntax-check the complete macOS WebGPU renderer against the packaged wgpu-native headers.""" +import os +import subprocess +import sys +import tempfile + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, os.path.join(ROOT, "third_party", "a64-kit", "tools")) +sys.path.insert(0, os.path.join(ROOT, "nx2")) +import build +import toolchain + + +def main(): + wgpu = os.environ["WGPU_NATIVE"] + cc = toolchain.need(toolchain.find("clang++")) + gpu = build.GPU + host = build.HOST + rt = os.path.join(ROOT, "third_party", "a64-kit", "rt") + with tempfile.TemporaryDirectory(prefix="hookshot-macos-generated-") as generated: + for name in ("a64_suyu.h", "a64_fpx.h"): + with open(os.path.join(generated, name), "w", encoding="utf-8") as f: + f.write("/* Generated game-specific helpers are unused by renderer sources. */\n") + flags = [ + *build.target(), "-mcpu=apple-m1", "-std=c++20", "-g0", "-DNDEBUG", "-Wno-everything", + "-DA64_WRITE_TRACK", + "-I" + os.path.join(gpu, "compat"), + "-I" + os.path.join(gpu, "third_party", "suyu"), + "-I" + os.path.join(wgpu, "include"), + "-I" + host, "-I" + rt, "-I" + build.POSIX, "-I" + generated, + "-include", os.path.join(gpu, "render", "rx_wgpu_names.h"), + ] + for source in build.gpu_sources(): + result = subprocess.run([cc] + flags + ["-fsyntax-only", source], text=True) + if result.returncode: + raise SystemExit("macOS renderer syntax check failed: " + os.path.relpath(source, ROOT)) + print("macOS WebGPU renderer sources compile against wgpu-native.") + + +if __name__ == "__main__": + main() diff --git a/scripts/macos/deps.sh b/scripts/macos/deps.sh new file mode 100644 index 000000000..7595aa311 --- /dev/null +++ b/scripts/macos/deps.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Build the native arm64 libraries and Python runtime used by Hookshot's macOS builder. +set -euo pipefail +if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then + echo "The macOS builder dependencies require an Apple Silicon macOS host." >&2 + exit 1 +fi +cd "$(dirname "$0")/../.." + +DEPS=${DEPS:-$PWD/build-macos/deps} +DL=$DEPS/downloads +mkdir -p "$DEPS" "$DL" +JOBS=$(sysctl -n hw.logicalcpu) + +PY_TAG=20261026 +PY_VER=3.12.16 +SDL_VER=2.32.10 +FFMPEG_VER=9.0.2 +DAV1D_VER=1.5.4 +WGPU_VER=v29.0.1.1 +MIN_MACOS=13.3 + +fetch() { + local url=$1 file=$2 + [ -s "$DL/$file" ] || curl -fL --retry 5 --retry-delay 10 --proto '=https' -o "$DL/$file.part" "$url" + [ -s "$DL/$file" ] || mv -f "$DL/$file.part" "$DL/$file" +} + +# ---- arm64 Python and the builder's modules ---- +if [ ! -x "$DEPS/python/bin/python3" ]; then + fetch "https://github.com/astral-sh/python-build-standalone/releases/download/$PY_TAG/cpython-$PY_VER%2B$PY_TAG-aarch64-apple-darwin-install_only.tar.gz" python.tar.gz + tar -xzf "$DL/python.tar.gz" -C "$DEPS" + "$DEPS/python/bin/python3" -m pip install --no-cache-dir --only-binary :all: \ + --disable-pip-version-check -q \ + "lz4==4.4.5" "numpy==2.5.4" "capstone==5.0.9" "unicorn==2.1.4" "zstandard==0.25.0" + rm -rf "$DEPS/python/lib/python3.12/test" "$DEPS/python/lib/python3.12/idlelib" \ + "$DEPS/python/lib/python3.12/ensurepip" + find "$DEPS/python" -name __pycache__ -prune -exec rm -rf {} + +fi + +# ---- build tools (cmake, meson, ninja, pkg-config) when the Mac has none (no Homebrew needed) ---- +BT=$DEPS/buildtools +if ! command -v cmake >/dev/null || ! command -v meson >/dev/null || ! command -v ninja >/dev/null \ + || ! command -v pkg-config >/dev/null; then + if [ ! -x "$BT/bin/meson" ]; then + rm -rf "$BT" && /usr/bin/python3 -m venv "$BT" + "$BT/bin/python3" -m pip install --disable-pip-version-check -q --only-binary :all: cmake ninja meson pkgconf + fi + [ -x "$BT/bin/pkg-config" ] || ln -sf pkgconf "$BT/bin/pkg-config" + export PATH="$BT/bin:$PATH" +fi + +# ---- SDL2, built as a native arm64 dylib ---- +if [ ! -f "$DEPS/sdl2/lib/libSDL2-2.0.0.dylib" ]; then + fetch "https://github.com/libsdl-org/SDL/releases/download/release-$SDL_VER/SDL2-$SDL_VER.tar.gz" sdl2.tar.gz + rm -rf "$DEPS/src-sdl2" "$DEPS/sdl2" + mkdir -p "$DEPS/src-sdl2" + tar -xzf "$DL/sdl2.tar.gz" -C "$DEPS/src-sdl2" --strip-components=1 + cmake -S "$DEPS/src-sdl2" -B "$DEPS/src-sdl2/build" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET="$MIN_MACOS" \ + -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TEST=OFF + cmake --build "$DEPS/src-sdl2/build" --parallel "$JOBS" + cmake --install "$DEPS/src-sdl2/build" --prefix "$DEPS/sdl2" + cp "$DEPS/src-sdl2/LICENSE.txt" "$DEPS/sdl2/" + rm -rf "$DEPS/src-sdl2" +fi + +# ---- dav1d and FFmpeg, static and position-independent for the game executable ---- +# (FFmpeg's own "av1" decoder only drives hardware decoders: without dav1d the game's AV1 movies are black) +P=$DEPS/ffmpeg +if [ ! -f "$P/lib/libavcodec.a" ] || [ ! -f "$P/lib/libdav1d.a" ]; then + rm -rf "$P" "$DEPS/src-ffmpeg" "$DEPS/src-dav1d" + fetch "https://code.videolan.org/videolan/dav1d/-/archive/$DAV1D_VER/dav1d-$DAV1D_VER.tar.gz" dav1d.tar.gz + mkdir -p "$DEPS/src-dav1d" + tar -xzf "$DL/dav1d.tar.gz" -C "$DEPS/src-dav1d" --strip-components=1 + ( + cd "$DEPS/src-dav1d" + CFLAGS="-mmacosx-version-min=$MIN_MACOS" LDFLAGS="-mmacosx-version-min=$MIN_MACOS" \ + meson setup build --prefix="$P" --libdir=lib --buildtype=release --default-library=static \ + -Db_staticpic=true -Denable_tools=false -Denable_tests=false -Denable_examples=false + ninja -C build install + ) + fetch "https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.xz" ffmpeg.tar.xz + mkdir -p "$DEPS/src-ffmpeg" + tar -xJf "$DL/ffmpeg.tar.xz" -C "$DEPS/src-ffmpeg" --strip-components=1 + ( + cd "$DEPS/src-ffmpeg" + CFLAGS="-arch arm64 -mcpu=apple-m1 -mmacosx-version-min=$MIN_MACOS -fPIC" \ + LDFLAGS="-arch arm64 -mmacosx-version-min=$MIN_MACOS" \ + PKG_CONFIG_PATH="$P/lib/pkgconfig" \ + ./configure --prefix="$P" --arch=aarch64 --cpu=apple-m1 --target-os=darwin \ + --cc=clang --enable-static --disable-shared --enable-pic --pkg-config-flags=--static \ + --disable-programs --disable-doc --disable-network --disable-autodetect \ + --disable-everything --disable-avformat --disable-avfilter --disable-avdevice \ + --enable-avcodec --enable-swscale --enable-swresample --enable-avutil \ + --enable-libdav1d --enable-decoder=vp8,vp9,libdav1d --enable-parser=vp8,vp9,av1 + make -j"$JOBS" + make install + ) + rm -rf "$DEPS/src-ffmpeg" "$DEPS/src-dav1d" "$P/share" + for lib in libavcodec.a libswscale.a libswresample.a libavutil.a libdav1d.a; do + lipo "$P/lib/$lib" -verify_arch arm64 + done +fi + +# ---- Metal-enabled arm64 wgpu-native release ---- +if [ ! -f "$DEPS/wgpu-native/lib/libwgpu_native.dylib" ]; then + fetch "https://github.com/gfx-rs/wgpu-native/releases/download/$WGPU_VER/wgpu-macos-aarch64-release.zip" wgpu.zip + rm -rf "$DEPS/wgpu-native" + mkdir -p "$DEPS/wgpu-native" + unzip -q "$DL/wgpu.zip" -d "$DEPS/wgpu-native" +fi +lipo "$DEPS/sdl2/lib/libSDL2-2.0.0.dylib" -verify_arch arm64 +lipo "$DEPS/wgpu-native/lib/libwgpu_native.dylib" -verify_arch arm64 + +# Keep the notices for the binary dependencies in the downloadable package. +L=$DEPS/licenses +mkdir -p "$L" +cp "$DEPS/sdl2/LICENSE.txt" "$L/SDL2-LICENSE.txt" +curl -fsSL --retry 5 -o "$L/FFmpeg-COPYING.LGPLv2.1" \ + "https://raw.githubusercontent.com/FFmpeg/FFmpeg/n$FFMPEG_VER/COPYING.LGPLv2.1" +curl -fsSL --retry 5 -o "$L/wgpu-native-LICENSE-APACHE" \ + "https://raw.githubusercontent.com/gfx-rs/wgpu-native/$WGPU_VER/LICENSE.APACHE" +curl -fsSL --retry 5 -o "$L/wgpu-native-LICENSE-MIT" \ + "https://raw.githubusercontent.com/gfx-rs/wgpu-native/$WGPU_VER/LICENSE.MIT" +echo "macOS builder dependencies: $(du -sh "$DEPS" | cut -f1)" diff --git a/scripts/macos/hookshot_env.txt b/scripts/macos/hookshot_env.txt new file mode 100644 index 000000000..a11488f2a --- /dev/null +++ b/scripts/macos/hookshot_env.txt @@ -0,0 +1,22 @@ +# Configurações do Hookshot para Mac (este arquivo é lido quando o jogo abre). +# Linhas que começam com # são comentários. Depois de mudar, feche e abra o jogo de novo. + +# ---- Qualidade ---- +# HOS_QUALITY escolhe um preset. Troque a palavra depois do = por uma destas: +# alta - o padrão do jogo no modo TV: resolução dinâmica e upscale FSR (a mais bonita) +# media - resolução fixa em 75% (mais leve para a GPU) +# baixa - perfil portátil do Switch 2: as resoluções mais leves do próprio jogo +# minima - perfil portátil em 50%, sem FSR (o mais leve; a imagem fica mais suave) +HOS_QUALITY=alta + +# ---- Ajustes finos (opcionais: tire o # do começo da linha para usar) ---- +# Resolução fixa, de 0.5 a 1.0 (vale mais que o preset): +#HOS_RES_SCALE=0.75 +# Perfil portátil (1) ou TV (0): +#HOS_HANDHELD=1 +# Upscale FSR na janela: 1 ligado, 0 desligado: +#RX_FSR=1 +# Velocidade do texto dos diálogos: 0 normal, 1 2x, 2 4x, 3 instantâneo: +#HOS_TEXT_SPEED=0 + +# Teclas durante o jogo (no Mac, segure fn junto): F8 liga/desliga o FSR, F9 muda a nitidez, F10 ajusta/estica a imagem, F11 tela cheia. diff --git a/scripts/macos/package-builder.sh b/scripts/macos/package-builder.sh new file mode 100644 index 000000000..fa88f9a52 --- /dev/null +++ b/scripts/macos/package-builder.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Assemble a downloadable macOS CLI builder; no game dump or game build is included. +set -euo pipefail +if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then + echo "The macOS builder package requires an Apple Silicon macOS host." >&2 + exit 1 +fi +cd "$(dirname "$0")/../.." + +DEPS=${DEPS:-$PWD/build-macos/deps} +OUT=${OUT:-$PWD/build-macos} +STAGE=$OUT/package +ROOT=$STAGE/HookshotBuilder +SOURCE=$ROOT/tools/source + +test -x "$DEPS/python/bin/python3" +test -f "$DEPS/sdl2/lib/libSDL2-2.0.0.dylib" +test -f "$DEPS/ffmpeg/lib/libavcodec.a" +test -f "$DEPS/wgpu-native/lib/libwgpu_native.dylib" +command -v lipo >/dev/null +for binary in "$DEPS/python/bin/python3" "$DEPS/sdl2/lib/libSDL2-2.0.0.dylib" \ + "$DEPS/wgpu-native/lib/libwgpu_native.dylib" "$DEPS/ffmpeg/lib/libavcodec.a" \ + "$DEPS/ffmpeg/lib/libswscale.a" "$DEPS/ffmpeg/lib/libswresample.a" "$DEPS/ffmpeg/lib/libavutil.a"; do + lipo "$binary" -verify_arch arm64 +done +rm -rf "$STAGE" +mkdir -p "$SOURCE" "$ROOT/tools" +git archive --format=tar HEAD | tar -xf - -C "$SOURCE" +cp -R "$DEPS/python" "$DEPS/sdl2" "$DEPS/ffmpeg" "$DEPS/wgpu-native" "$DEPS/licenses" "$ROOT/tools/" +cp LICENSE "$ROOT/LICENSE" + +cat > "$ROOT/HookshotBuilder.command" <<'EOF' +#!/bin/sh +set -eu +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +TOOLS="$ROOT/tools" +export WGPU_NATIVE="$TOOLS/wgpu-native" +export HOOKSHOT_SDL2="$TOOLS/sdl2/lib/libSDL2-2.0.0.dylib" +export HOOKSHOT_FFMPEG="$TOOLS/ffmpeg" +export RX_BACKEND=metal +exec "$TOOLS/python/bin/python3" -I "$TOOLS/source/nx2/make_game.py" "$@" +EOF +chmod +x "$ROOT/HookshotBuilder.command" +cp scripts/macos/README.txt "$ROOT/README.txt" + +APP=$ROOT/HookshotBuilder.app +osacompile -o "$APP" scripts/macos/Builder.applescript +mkdir -p "$APP/Contents/Resources" +mv "$ROOT/tools" "$APP/Contents/Resources/tools" +ln -s "HookshotBuilder.app/Contents/Resources/tools" "$ROOT/tools" +test -x "$APP/Contents/MacOS/applet" +lipo "$APP/Contents/MacOS/applet" -verify_arch arm64 + +mkdir -p "$OUT" +cd "$STAGE" +ditto -c -k --sequesterRsrc --keepParent HookshotBuilder "$OUT/HookshotBuilder-macos-arm64.zip" +ditto -c -k --sequesterRsrc --keepParent "$ROOT/HookshotBuilder.app" "$OUT/HookshotBuilder-macos-arm64.app.zip" +echo "Created $OUT/HookshotBuilder-macos-arm64.zip" +echo "Created $OUT/HookshotBuilder-macos-arm64.app.zip" diff --git a/tests/run_all.py b/tests/run_all.py index 431618edd..a7f8108a8 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -20,8 +20,19 @@ import time ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -WINDOWS = os.name == "nt" -OS = "windows" if WINDOWS else "linux" + + +def host_os(): + if sys.platform == "win32": + return "windows" + if sys.platform == "darwin": + return "macos" + if sys.platform.startswith("linux"): + return "linux" + raise RuntimeError("Unsupported host platform: " + sys.platform) + + +OS = host_os() # Arguments some tests need. {name} is a tool or folder found below (find_tools); a check whose tools are missing is # skipped, or fails with --strict. Tests not listed run with no arguments and their own defaults. @@ -74,15 +85,15 @@ SKIP = { "tests/test_precompile_compute_guard.py": ({"linux"}, "Windows structured exceptions"), # (the game: these run the lifted code of a real dump against Unicorn) - "nx2/test_insn.py": ({"linux", "windows"}, "needs the game's modules (work/dump) and Unicorn"), - "nx2/test_leaf.py": ({"linux", "windows"}, "needs the game's modules (work/dump) and Unicorn"), - "nx2/test_dll.py": ({"linux", "windows"}, "needs a full game build (work/build/hookshot_game.dll) and Unicorn"), + "nx2/test_insn.py": ({"linux", "windows", "macos"}, "needs the game's modules (work/dump) and Unicorn"), + "nx2/test_leaf.py": ({"linux", "windows", "macos"}, "needs the game's modules (work/dump) and Unicorn"), + "nx2/test_dll.py": ({"linux", "windows", "macos"}, "needs a full game build (work/build/hookshot_game.dll) and Unicorn"), # (a GPU) - "nx2/test_bc_storage_views.py": ({"linux", "windows"}, "needs a Windows Vulkan device"), + "nx2/test_bc_storage_views.py": ({"linux", "windows", "macos"}, "needs a Windows Vulkan device"), # (Linux only) - "tests/test_hookshot_launch.py": ({"windows"}, "the Linux launcher, a POSIX shell script"), - "tests/test_linux_update.py": ({"windows"}, "the Linux builder's update and folder launcher, POSIX files and shell scripts"), - "tests/test_linux_driver_fault_guard.py": ({"windows"}, "POSIX signals: the Linux guard"), + "tests/test_hookshot_launch.py": ({"windows", "macos"}, "the Linux launcher, a POSIX shell script"), + "tests/test_linux_update.py": ({"windows", "macos"}, "the Linux builder's update and folder launcher, POSIX files and shell scripts"), + "tests/test_linux_driver_fault_guard.py": ({"windows", "macos"}, "POSIX signals: the Linux guard"), } # (spirv-val: SPIRV-Tools comes with the Linux runner's packages, not on Windows) for _name in ("nx2/test_depth_sample_types.py", "nx2/test_default_point_size.py", "nx2/test_draw_parameters.py", "nx2/test_coop_spirv.py", diff --git a/tests/test_cpu_target.py b/tests/test_cpu_target.py index c4d2f1ec1..f73c6093b 100644 --- a/tests/test_cpu_target.py +++ b/tests/test_cpu_target.py @@ -69,7 +69,8 @@ def __init__(self, present): def test_build_and_appimage_use_it(self): build = (ROOT / "nx2" / "build.py").read_text(encoding="utf-8") self.assertIn("from host_cpu import host_march", build) - self.assertIn('MARCH = os.environ.get("RX_MARCH") or host_march()', build) + self.assertIn('MARCH = os.environ.get("RX_MARCH") or ("apple-m1" if MACOS else host_march())', build) + self.assertIn('if not MACOS and MARCH != "x86-64-v3":', build) appimage = (ROOT / "scripts" / "linux" / "appimage.sh").read_text(encoding="utf-8") self.assertIn("RX_MARCH=x86-64-v3 RX_VK_ONLY=1", appimage) # (its prebuilt renderer: not the CI runner's CPU) diff --git a/tests/test_finish_output.py b/tests/test_finish_output.py index b1d9ef586..af04b6b55 100644 --- a/tests/test_finish_output.py +++ b/tests/test_finish_output.py @@ -70,8 +70,8 @@ def finish(self, launcher=False): with mock.patch.object(make_game, "BESIDE", ["hookshot.exe"]): # Comprehensions use globals. Keep one namespace, as in production, and discover only authored setup files. namespace = dict(make_game.__dict__, built=str(self.built), out=str(self.out), work=str(self.work), - ROOT=str(self.root / "tools"), LINUX=False if launcher else make_game.LINUX) - exec(FINISH, namespace) + game_dir=str(self.out), launcher=None, ROOT=str(self.root / "tools"), LINUX=False) + exec(FINISH, namespace, namespace) def assert_installed_unchanged(self): self.assertEqual(self.destination.read_bytes(), b"existing synthetic executable") diff --git a/tests/test_macos_build_path.py b/tests/test_macos_build_path.py new file mode 100644 index 000000000..b857293a9 --- /dev/null +++ b/tests/test_macos_build_path.py @@ -0,0 +1,189 @@ +"""Verify the macOS branch of the production build configuration without a game dump.""" +import json +import subprocess +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +class MacOSBuildPathTests(unittest.TestCase): + def test_macos_toolchain_uses_the_xcrun_sdk(self): + source = ROOT / "third_party" / "a64-kit" / "tools" / "toolchain.py" + script = r""" +import runpy, sys, tempfile, types +from pathlib import Path +from unittest.mock import patch +sys.platform = "darwin" +tools = runpy.run_path(sys.argv[1]) +with tempfile.TemporaryDirectory() as sdk: + with patch.dict("os.environ", {"SDKROOT": ""}), patch("shutil.which", return_value="xcrun"), \ + patch("subprocess.run", return_value=types.SimpleNamespace(returncode=0, stdout=sdk, stderr="")) as run: + assert tools["macos_sysroot_flags"]() == ["-isysroot", sdk] + run.assert_called_once_with(["xcrun", "--sdk", "macosx", "--show-sdk-path"], + capture_output=True, text=True) +""" + subprocess.run([sys.executable, "-I", "-c", script, str(source)], check=True, + capture_output=True, text=True) + + def test_selects_native_arm64_posix_and_metal_build_target(self): + source = ROOT / "nx2" / "build.py" + script = ( + "import json, runpy, sys; " + "sys.platform = 'darwin'; " + "b = runpy.run_path(sys.argv[1]); " + "b['toolchain'].macos_sysroot_flags = lambda: ['-isysroot', '/MacOSX.sdk']; " + "print(json.dumps({k: b[k] for k in ('WINDOWS', 'LINUX', 'MACOS', 'POSIX_HOST', 'EXE', 'MARCH', 'CPU')} " + "| {'target': b['target']()}))" + ) + result = subprocess.run([sys.executable, "-I", "-c", script, str(source)], + check=True, capture_output=True, text=True) + config = json.loads(result.stdout.splitlines()[-1]) + self.assertEqual(config, { + "WINDOWS": False, + "LINUX": False, + "MACOS": True, + "POSIX_HOST": True, + "EXE": "hookshot", + "MARCH": "apple-m1", + "CPU": ["-mcpu=apple-m1", "-ffp-contract=off"], + "target": ["--target=arm64-apple-macos13.3", "-isysroot", "/MacOSX.sdk"], + }) + + def test_game_builder_selects_macos_bundle_files(self): + source = ROOT / "nx2" / "make_game.py" + script = ( + "import json, runpy, sys; " + "sys.platform = 'darwin'; " + "b = runpy.run_path(sys.argv[1]); " + "print(json.dumps({k: b[k] for k in ('LINUX', 'MACOS', 'BESIDE')}))" + ) + result = subprocess.run([sys.executable, "-I", "-c", script, str(source)], + check=True, capture_output=True, text=True) + config = json.loads(result.stdout.splitlines()[-1]) + self.assertEqual(config, { + "LINUX": False, + "MACOS": True, + "BESIDE": ["hookshot", "libSDL2-2.0.0.dylib", "libwgpu_native.dylib"], + }) + + def test_macos_preflight_verifies_dependency_architectures(self): + source = ROOT / "nx2" / "make_game.py" + script = r""" +import os, runpy, sys, tempfile, types +from pathlib import Path +from unittest.mock import patch +sys.platform = "darwin" +builder = runpy.run_path(sys.argv[1]) +with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + wgpu = root / "wgpu" + header = wgpu / "include" / "webgpu" / "webgpu.h" + library = wgpu / "lib" / "libwgpu_native.dylib" + sdl = root / "libSDL2-2.0.0.dylib" + for path in (header, library, sdl): + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + commands = [] + def run(command, **kwargs): + commands.append(command) + return types.SimpleNamespace(returncode=0, stdout="", stderr="") + toolchain = types.SimpleNamespace(find=lambda name: sys.executable, + macos_target_flags=lambda: ["--target=arm64-apple-macos13.3"], + macos_sysroot_flags=lambda: ["-isysroot", "/MacOSX.sdk"]) + with patch.dict(os.environ, {"WGPU_NATIVE": str(wgpu), "HOOKSHOT_SDL2": str(sdl), "HOOKSHOT_FFMPEG": ""}), \ + patch("shutil.which", return_value="tool"), \ + patch("subprocess.run", side_effect=run), \ + patch("platform.machine", return_value="arm64"), \ + patch.dict(sys.modules, {"toolchain": toolchain}): + builder["check_macos_toolchain"]() + assert [c for c in commands if c[0] == "lipo"] == [ + ["lipo", str(sdl), "-verify_arch", "arm64"], + ["lipo", str(library), "-verify_arch", "arm64"], + ] +""" + subprocess.run([sys.executable, "-I", "-c", script, str(source)], check=True, + capture_output=True, text=True) + + def test_macos_preflight_accepts_bundled_static_ffmpeg(self): + source = ROOT / "nx2" / "make_game.py" + script = r""" +import os, runpy, sys, tempfile, types +from pathlib import Path +from unittest.mock import patch +sys.platform = "darwin" +builder = runpy.run_path(sys.argv[1]) +with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + wgpu = root / "wgpu" + sdl = root / "libSDL2-2.0.0.dylib" + ffmpeg = root / "ffmpeg" + for path in (wgpu / "include/webgpu/webgpu.h", wgpu / "lib/libwgpu_native.dylib", sdl): + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + headers = ("libavcodec/avcodec.h", "libswscale/swscale.h", "libswresample/swresample.h", + "libavutil/avutil.h") + libs = ("libavcodec.a", "libswscale.a", "libswresample.a", "libavutil.a") + for relative in headers + libs: + path = ffmpeg / ("include" if relative.endswith(".h") else "lib") / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + commands = [] + def run(command, **kwargs): + commands.append(command) + return types.SimpleNamespace(returncode=0, stdout="", stderr="") + toolchain = types.SimpleNamespace(find=lambda name: sys.executable, + macos_target_flags=lambda: ["--target=arm64-apple-macos13.3"], + macos_sysroot_flags=lambda: ["-isysroot", "/MacOSX.sdk"]) + env = {"WGPU_NATIVE": str(wgpu), "HOOKSHOT_SDL2": str(sdl), "HOOKSHOT_FFMPEG": str(ffmpeg)} + with patch.dict(os.environ, env), patch("shutil.which", return_value="tool"), \ + patch("subprocess.run", side_effect=run), patch("platform.machine", return_value="arm64"), \ + patch.dict(sys.modules, {"toolchain": toolchain}): + builder["check_macos_toolchain"]() + assert [c for c in commands if c[0] == "lipo"] == [ + ["lipo", str(sdl), "-verify_arch", "arm64"], + ["lipo", str(wgpu / "lib/libwgpu_native.dylib"), "-verify_arch", "arm64"], + ] + [["lipo", str(ffmpeg / "lib" / lib), "-verify_arch", "arm64"] for lib in libs] + assert not any(c[0] == "pkg-config" for c in commands) +""" + subprocess.run([sys.executable, "-I", "-c", script, str(source)], check=True, + capture_output=True, text=True) + + def test_macos_runtime_supplies_non_linux_profiler_entry_point(self): + source = (ROOT / "host" / "posix" / "macos_stubs.c").read_text(encoding="utf-8") + self.assertIn("void hos_prof_start(void)", source) + + def test_native_arm64_path_has_host_context_and_memory_ordering_support(self): + context = (ROOT / "host" / "hos_setjmp.c").read_text(encoding="utf-8") + lifter = (ROOT / "third_party" / "a64-kit" / "lift" / "suyu" / "arm64_to_c.h").read_text(encoding="utf-8") + self.assertIn("#elif defined(__aarch64__) || defined(__arm64__)", context) + self.assertIn("stp x19, x20", context) + self.assertIn("__atomic_thread_fence(__ATOMIC_ACQUIRE)", lifter) + self.assertIn("__atomic_thread_fence(__ATOMIC_RELEASE)", lifter) + + def test_macos_renderer_uses_sdl_pixel_dimensions(self): + renderer = (ROOT / "gpu" / "render" / "rx_core.cpp").read_text(encoding="utf-8") + window = (ROOT / "host" / "posix" / "display_sdl.c").read_text(encoding="utf-8") + sdl = (ROOT / "host" / "hos_sdl.c").read_text(encoding="utf-8") + self.assertIn("hos_posix_window_pixel_size(&width, &height)", renderer) + self.assertIn("void hos_posix_window_pixel_size(int *width, int *height)", window) + self.assertIn("window_pixel_size(window, width, height)", window) + renderer_thread = window[window.index("static DWORD WINAPI renderer_main"):window.index("void hos_renderer_wait")] + display_loop = window[window.index("void hos_display_loop"): ] + self.assertNotIn("hos_sdl_metal_create_view", renderer_thread) + self.assertLess(display_loop.index("hos_sdl_metal_create_view(window)"), display_loop.index("CreateThread")) + self.assertIn('GET(Metal_CreateView, "SDL_Metal_CreateView")', sdl) + self.assertIn("WIN_METAL", sdl) + + def test_linux_guest_memory_optimizations_are_guarded_from_macos(self): + source = (ROOT / "host" / "hos_main.c").read_text(encoding="utf-8") + self.assertIn("#ifdef __linux__\n /* The window is a memfd's", source) + self.assertIn("#ifdef __linux__\n MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE | MAP_NORESERVE,", source) + self.assertIn("#ifdef __linux__\n if (!getenv(\"HOS_THP\")) madvise", source) + self.assertIn("#ifdef __linux__\n return madvise(p, n, hos_guest_memfd >= 0 ? MADV_REMOVE", source) + self.assertIn(" return 1;\n#endif\n}", source) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_toolchain_platform.py b/tests/test_toolchain_platform.py new file mode 100644 index 000000000..57550fd3c --- /dev/null +++ b/tests/test_toolchain_platform.py @@ -0,0 +1,43 @@ +"""Host-versus-target tool selection for native helpers and cross-builds.""" +import importlib.util +from pathlib import Path +import sys +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +TOOLCHAIN = ROOT / "third_party" / "a64-kit" / "tools" / "toolchain.py" +SPEC = importlib.util.spec_from_file_location("hookshot_toolchain_platform_test", TOOLCHAIN) +toolchain = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(toolchain) + +RUNNER = ROOT / "tests" / "run_all.py" +RUNNER_SPEC = importlib.util.spec_from_file_location("hookshot_regression_runner_platform_test", RUNNER) +runner = importlib.util.module_from_spec(RUNNER_SPEC) +RUNNER_SPEC.loader.exec_module(runner) + + +class ToolchainPlatformTests(unittest.TestCase): + def test_find_uses_host_tool_for_native_helper_during_cross_build(self): + cross = str(ROOT / "fake-llvm-mingw") + cross_clang = toolchain.os.path.join(cross, "bin", "clang.exe") + + def mingw(host=False): + return "" if host else cross + + with mock.patch.object(toolchain.sys, "platform", "linux"), \ + mock.patch.object(toolchain, "mingw", side_effect=mingw), \ + mock.patch.object(toolchain.os.path, "isfile", side_effect=lambda path: path == cross_clang), \ + mock.patch.object(toolchain.shutil, "which", side_effect=lambda name: "host-" + name): + self.assertEqual(toolchain.find("clang"), cross_clang) + self.assertEqual(toolchain.find("clang", host=True), "host-clang") + + def test_regression_runner_identifies_each_supported_host(self): + for platform, expected in (("win32", "windows"), ("linux", "linux"), ("darwin", "macos")): + with self.subTest(platform=platform), mock.patch.object(runner.sys, "platform", platform): + self.assertEqual(runner.host_os(), expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/third_party/a64-kit/lift/build_emit.py b/third_party/a64-kit/lift/build_emit.py index d077ce5fd..7b6fcc8aa 100644 --- a/third_party/a64-kit/lift/build_emit.py +++ b/third_party/a64-kit/lift/build_emit.py @@ -1,6 +1,6 @@ """Build a64emit (suyu's AArch64 -> C instruction translator) as a shared library for a64_lift.py. -Usage: python build_emit.py -> a64emit.dll (Windows) / liba64emit.so (elsewhere), next to this file +Usage: python build_emit.py -> a64emit.dll (Windows) / liba64emit.dylib (macOS) / liba64emit.so (elsewhere) Rebuilds only when a64emit.cpp or suyu/arm64_to_c.h changed. """ import hashlib, os, subprocess, sys @@ -10,7 +10,8 @@ import toolchain SRC = [os.path.join(HERE, "a64emit.cpp"), os.path.join(HERE, "suyu", "arm64_to_c.h")] -OUT = os.path.join(HERE, "a64emit.dll" if os.name == "nt" else "liba64emit.so") +OUT = os.path.join(HERE, "a64emit.dll" if os.name == "nt" else + "liba64emit.dylib" if sys.platform == "darwin" else "liba64emit.so") def build(force=False): @@ -18,8 +19,11 @@ def build(force=False): stamp = OUT + ".sha1" if not force and os.path.exists(OUT) and os.path.exists(stamp) and open(stamp).read() == key: return OUT - cmd = [toolchain.need(toolchain.find("clang++", host=True)), "-std=c++20", "-O2", "-shared", "-o", OUT, SRC[0], - "-Wno-everything"] + toolchain.native_target(host=True) + toolchain.native_link(host=True) + target = (toolchain.macos_target_flags() + toolchain.macos_sysroot_flags() + if sys.platform == "darwin" else toolchain.native_target()) + cmd = [toolchain.need(toolchain.find("clang++")), "-std=c++20", "-O2", + "-dynamiclib" if sys.platform == "darwin" else "-shared", "-o", OUT, SRC[0], + "-Wno-everything"] + target + toolchain.native_link() if os.name != "nt": cmd += ["-fPIC"] print("building", os.path.basename(OUT)) diff --git a/third_party/a64-kit/lift/suyu/arm64_to_c.h b/third_party/a64-kit/lift/suyu/arm64_to_c.h index 1996ad9f3..66acf50ee 100644 --- a/third_party/a64-kit/lift/suyu/arm64_to_c.h +++ b/third_party/a64-kit/lift/suyu/arm64_to_c.h @@ -1629,10 +1629,9 @@ inline bool Translate(u32 i, u64 pc, std::string& out, bool* unhandled = nullptr // Load/store exclusive and acquire/release. // - // The non-exclusive acquire/release forms (LDAR/STLR, o2 set) are ordinary - // loads and stores as far as this backend is concerned - suyu's memory - // accessors are already atomic at these widths, and there is no weaker - // ordering here to fence against - so they are translated directly. + // The non-exclusive acquire/release forms (LDAR/STLR, o2 set) use the + // backend's normal atomic accessors, with explicit fences for AArch64's + // acquire/release ordering on weakly ordered hosts. // // The genuinely exclusive forms (LDXR/LDAXR/STXR/STLXR, o2 clear) are not. // They used to become a plain load/store with STXR unconditionally @@ -1644,6 +1643,7 @@ inline bool Translate(u32 i, u64 pc, std::string& out, bool* unhandled = nullptr // to the fallback engine, which owns the kernel's real exclusive monitor. if ((i & 0x3F000000) == 0x08000000) { const u32 size = i >> 30, o2 = (i >> 23) & 1, L = (i >> 22) & 1, o1 = (i >> 21) & 1; + const u32 o0 = (i >> 15) & 1; const u32 rt2 = (i >> 10) & 31, rn = (i >> 5) & 31, rt = i & 31; // Pair forms: LDXP/LDAXP/STXP/STLXP. size 00/01 is unallocated here, // so only the 32- and 64-bit register widths exist. @@ -1663,6 +1663,7 @@ inline bool Translate(u32 i, u64 pc, std::string& out, bool* unhandled = nullptr // doubleword at the address. std::string s2 = "{ uint64_t _lo,_hi; recomp_ldxp(c," + addr + "," + sz + ",&_lo,&_hi); "; + if (o0) s2 += "__atomic_thread_fence(__ATOMIC_ACQUIRE); "; // A W-register destination is written zero-extended. const char* cast = (rbytes == 4) ? "(uint32_t)" : ""; if (rt != 31) { @@ -1680,7 +1681,9 @@ inline bool Translate(u32 i, u64 pc, std::string& out, bool* unhandled = nullptr (rbytes == 4) ? ("(uint64_t)(uint32_t)" + Xz(rt)) : Xz(rt); const std::string hi = (rbytes == 4) ? ("(uint64_t)(uint32_t)" + Xz(rt2)) : Xz(rt2); - std::string s2 = "{ uint32_t _st = recomp_stxp(c," + addr + "," + sz + "," + + std::string s2 = "{ "; + if (o0) s2 += "__atomic_thread_fence(__ATOMIC_RELEASE); "; + s2 += "uint32_t _st = recomp_stxp(c," + addr + "," + sz + "," + lo + "," + hi + "); "; if (rs != 31) { s2 += "c->x[" + std::to_string(rs) + "] = _st; "; @@ -1697,25 +1700,26 @@ inline bool Translate(u32 i, u64 pc, std::string& out, bool* unhandled = nullptr // own exclusive monitor, so an exclusive taken here is visible // to a thread running on the fallback JIT. // - // Acquire/release ordering (the LDAXR/STLXR spelling, o0 set) - // is not modelled separately: suyu's memory accessors are - // already atomic at these widths and the host is x86-64, whose - // ordering is strong enough that the fence these imply is a - // no-op in practice. Noted rather than silently assumed. const std::string addr = "c->x[" + std::to_string(rn) + "]"; if (L) { // LDXR/LDAXR: mark and load. + std::string s = "{ uint64_t _v = recomp_ldxr(c," + addr + "," + + std::to_string(bits / 8) + "); "; + if (o0) s += "__atomic_thread_fence(__ATOMIC_ACQUIRE); "; if (rt != 31) { - put("c->x[" + std::to_string(rt) + "] = recomp_ldxr(c," + addr + "," + - std::to_string(bits / 8) + ");"); + s += "c->x[" + std::to_string(rt) + "] = _v; "; } else { - put("(void)recomp_ldxr(c," + addr + "," + std::to_string(bits / 8) + ");"); + s += "(void)_v; "; } + s += "}"; + put(s); } else { // STXR/STLXR: Rs receives the status, 0 on success. Rs is // held in the Rt2 field's neighbour (bits 20:16). const u32 rs = (i >> 16) & 31; - std::string s2 = "{ uint32_t _st = recomp_stxr(c," + addr + "," + + std::string s2 = "{ "; + if (o0) s2 += "__atomic_thread_fence(__ATOMIC_RELEASE); "; + s2 += "uint32_t _st = recomp_stxr(c," + addr + "," + std::to_string(bits / 8) + "," + Xz(rt) + "); "; if (rs != 31) { s2 += "c->x[" + std::to_string(rs) + "] = _st; "; @@ -1729,12 +1733,14 @@ inline bool Translate(u32 i, u64 pc, std::string& out, bool* unhandled = nullptr if (L) { // LDAR if (rt != 31) { - put("c->x[" + std::to_string(rt) + "] = recomp_load" + - std::to_string(bits) + "(c," + addr + ");"); + put("{ uint64_t _v = recomp_load" + std::to_string(bits) + "(c," + addr + + "); __atomic_thread_fence(__ATOMIC_ACQUIRE); c->x[" + + std::to_string(rt) + "] = _v; }"); } } else { // STLR - no status register. - put("recomp_store" + std::to_string(bits) + "(c," + addr + "," + Xz(rt) + ");"); + put("{ __atomic_thread_fence(__ATOMIC_RELEASE); recomp_store" + + std::to_string(bits) + "(c," + addr + "," + Xz(rt) + "); }"); } return true; } @@ -6557,7 +6563,7 @@ inline const char* FpxH() { #if defined(RECOMP_NO_NATIVE_FP) || defined(__FAST_MATH__) || defined(_M_FP_FAST) || \ (defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0) #define RECOMP_FPX_HOST 0 /* compiled out: every op takes the exact body */ -#elif defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) +#elif (defined(__aarch64__) || defined(__arm64__)) && (defined(__GNUC__) || defined(__clang__)) #define RECOMP_FPX_HOST 1 /* the host FPU is the guest's */ #elif ((defined(__x86_64__) || defined(_M_X64)) && !defined(_M_ARM64EC)) || (defined(__wasm__) && defined(__SSE2__)) #define RECOMP_FPX_HOST 2 /* SSE2 (wasm: Emscripten's SSE2 on wasm SIMD; its float ops are IEEE round-to-nearest too) */ diff --git a/third_party/a64-kit/tools/toolchain.py b/third_party/a64-kit/tools/toolchain.py index 450d4fd63..3e8c392ac 100644 --- a/third_party/a64-kit/tools/toolchain.py +++ b/third_party/a64-kit/tools/toolchain.py @@ -1,4 +1,4 @@ -import os, shutil +import os, shutil, subprocess, sys HERE = os.path.dirname(os.path.abspath(__file__)) @@ -7,10 +7,14 @@ def emsdk(): return os.environ.get("EMSDK") or (r"C:\emsdk" if os.name == "nt" else os.path.expanduser("~/emsdk")) +def macos(): + return sys.platform == "darwin" + + def windows(): """(hookshot) the game is built for Windows: on Windows, or with RX_TARGET=windows on another system (a cross build, with llvm-mingw's release for that system in HOOKSHOT_MINGW)""" - return os.name == "nt" or os.environ.get("RX_TARGET", "").lower() == "windows" + return sys.platform == "win32" or os.environ.get("RX_TARGET", "").lower() == "windows" def mingw(host=False): @@ -21,8 +25,17 @@ def mingw(host=False): def find(tool, host=False): - if mingw(host) and (tool in ("clang", "clang++", "lld") or tool.startswith("llvm-")): - path = os.path.join(mingw(host), "bin", ("ld.lld" if tool == "lld" else tool) + (".exe" if os.name == "nt" else "")) + if macos() and tool in ("clang", "clang++") and not mingw(host): + xcrun = shutil.which("xcrun") + if xcrun: + result = subprocess.run([xcrun, "--find", tool], capture_output=True, text=True) + if result.returncode == 0: + path = result.stdout.strip() + if path: + return path + mingw_toolchain = mingw(host) + if mingw_toolchain and (tool in ("clang", "clang++", "lld") or tool.startswith("llvm-")): + path = os.path.join(mingw_toolchain, "bin", ("ld.lld" if tool == "lld" else tool) + ".exe") if os.path.isfile(path): return path root = emsdk() @@ -36,6 +49,30 @@ def find(tool, host=False): return shutil.which(tool) or os.path.join(folder, tool + exts[0]) +def macos_sysroot_flags(): + """Return the active macOS SDK for direct clang invocations (xcrun normally adds this implicitly).""" + if sys.platform != "darwin": + return [] + sdk = os.environ.get("SDKROOT", "") + if sdk and os.path.isdir(sdk): + return ["-isysroot", sdk] + xcrun = shutil.which("xcrun") + if not xcrun: + raise SystemExit("Xcode Command Line Tools are required to locate the macOS SDK (xcrun not found).") + result = subprocess.run([xcrun, "--sdk", "macosx", "--show-sdk-path"], capture_output=True, text=True) + sdk = result.stdout.strip() + if result.returncode or not sdk or not os.path.isdir(sdk): + reason = result.stderr.strip() or "xcrun returned no SDK path" + raise SystemExit("Could not locate the macOS SDK: %s" % reason) + return ["-isysroot", sdk] + + +def macos_target_flags(): + if sys.platform != "darwin": + return [] + return ["--target=arm64-apple-macos13.3"] + + def need(path): if not os.path.isfile(path): raise SystemExit("%s not found: set EMSDK to an emsdk checkout (now %s), or put %s on PATH"