Merge PR #16 (Sanvtto): macOS on Apple silicon, x86_64 under Rosetta 2 with MoltenVK (experimental)

Merged into the work branch as an experimental platform: the changes are
behind __APPLE__ or type-correct casts; the Linux build passes with them.
Not yet run with the game on a Mac.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Eij6gW2nnTUK9BowHuBVZZ
This commit is contained in:
Claude committed 2026-10-30 10:55:33 -03:00
commit 0b2352a1eb
30 files changed
+820 -38

No files matched your search

+29 -4
View File
@@ -48,6 +48,31 @@ MINGW*|MSYS*)
exit 0
;;
esac
# Linux defaults; macOS replaces them below.
no_pie=(-no-pie) export_dynamic=-rdynamic origin='$ORIGIN' heap=() cmake_platform=()
# macOS: an x86_64 build that Rosetta 2 runs, Vulkan through MoltenVK. Dependencies come from
# packaging/macos/build-deps-x86_64.sh (ISSN_MAC_DEPS), never from the arm64 Homebrew.
if [[ $(uname -s) == Darwin ]]; then
deps=${ISSN_MAC_DEPS:-$PWD/out/macos-deps}
[[ -f $deps/lib/libMoltenVK.dylib ]] || { echo "macOS dependencies missing: run packaging/macos/build-deps-x86_64.sh" >&2; exit 1; }
export MACOSX_DEPLOYMENT_TARGET=13.0 PATH="$deps/bin:$PATH"
export PKG_CONFIG_LIBDIR="$deps/lib/pkgconfig:$deps/share/pkgconfig"
printf '#!/bin/sh\nexec clang -arch x86_64 -mmacosx-version-min=13.0 "$@"\n' > out/cc-x86_64
chmod +x out/cc-x86_64
CC=$PWD/out/cc-x86_64
# The guest packs pointers into 40 bits: mimalloc, patched to map in 32-64 GiB, replaces
# the system malloc (near 128 TiB). It must come first on the link line.
no_pie=() export_dynamic=-Wl,-export_dynamic,-rpath,"$deps/lib" origin=@loader_path
heap=("$deps/lib/mimalloc-3.5/mimalloc.o" src/compat_mac.c)
# shadPS4 points the Vulkan loader at the executable's directory (VK_DRIVER_FILES).
printf '{"file_format_version":"1.0.0","ICD":{"library_path":"%s","api_version":"1.4.0","is_portability_driver":true}}\n' \
"$deps/lib/libMoltenVK.dylib" > out/MoltenVK_icd.json
boost=$(ls -d /opt/homebrew/lib/cmake/Boost-* 2>/dev/null | tail -1)
cmake_platform=(-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0
-DCMAKE_PREFIX_PATH="$deps" -DCMAKE_IGNORE_PREFIX_PATH=/opt/homebrew
-DBoost_DIR="$boost" -DCMAKE_POLICY_VERSION_MINIMUM=3.5)
ISSN_PGO=${ISSN_PGO:-off} ISSN_LTO=${ISSN_LTO:-OFF}
fi
if [[ -z ${CC:-} ]]; then
CC=$(command -v cc || command -v gcc || true)
if [[ -z $CC ]]; then
@@ -93,7 +118,7 @@ for patch in gpu/patches/fsr-vulkan/*.patch; do
fi
done
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DISSN_PGO="$pgo" \
-DISSN_LTO="${ISSN_LTO:-ON}" -DISSN_PGO_DIR="$PWD/pgo" >/dev/null
-DISSN_LTO="${ISSN_LTO:-ON}" -DISSN_PGO_DIR="$PWD/pgo" ${cmake_platform[@]+"${cmake_platform[@]}"} >/dev/null
echo "GPU library: PGO $pgo, LTO ${ISSN_LTO:-ON}"
# A failed GPU build must stop here: an older libissngpu.so would otherwise be used silently.
if ! ninja -C out/gpu issngpu > out/gpu-build.log 2>&1; then
@@ -101,7 +126,7 @@ if ! ninja -C out/gpu issngpu > out/gpu-build.log 2>&1; then
echo 'GPU library build failed (full log: out/gpu-build.log)' >&2; exit 1
fi
# $ORIGIN/gpu: packaged copies keep the library next to the binary without patching it.
gpu=(-Lout/gpu -lissngpu -Wl,-rpath,'$ORIGIN/gpu' -Wl,-rpath,"$PWD/out/gpu" -rdynamic)
gpu=(-Lout/gpu -lissngpu -Wl,-rpath,"$origin/gpu" -Wl,-rpath,"$PWD/out/gpu" "$export_dynamic")
runtime=(src/runtime*.c)
# Third-party decoders: compiled once, without this project's -Werror policy.
atrac9=(third_party/LibAtrac9/C/src/*.c)
@@ -112,10 +137,10 @@ if [[ ! -f out/libatrac9.a || -n $(find third_party/LibAtrac9/C/src -newer out/l
fi
# isa_emulate runs inside guest code: no XMM/YMM.
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -mgeneral-regs-only -c src/isa_emulate.c -o out/isa_emulate.o
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -no-pie "${includes[@]}" -I. -Isrc src/probe.c "${runtime[@]}" src/cpu_check.c src/vulkan_smoke.c out/isa_emulate.o src/isa_rewrite.c out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/issn-probe
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread ${no_pie[@]+"${no_pie[@]}"} "${includes[@]}" -I. -Isrc ${heap[@]+"${heap[@]}"} src/probe.c "${runtime[@]}" src/cpu_check.c src/vulkan_smoke.c out/isa_emulate.o src/isa_rewrite.c out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/issn-probe
echo "Built $PWD/out/issn-probe"
# GPU check for run.sh (live_resolution=auto): links only the Vulkan loader.
"$CC" -std=c11 -O2 -Wall -Wextra -Werror -Igpu/shadps4 tools/gpu_capabilities.c "${libraries[@]}" -o out/issn-gpu-capabilities
"$CC" -std=c11 -O2 -Wall -Wextra -Werror "${includes[@]}" -Igpu/shadps4 tools/gpu_capabilities.c "${libraries[@]}" -o out/issn-gpu-capabilities
if [[ ${1:-} == --test ]]; then
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -lm -o out/pad-test
out/pad-test
+7 -2
View File
@@ -82,7 +82,8 @@ endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
if (NOT WIN32)
# macOS presents through MoltenVK's Metal surface (SDL), with no X11.
if (NOT WIN32 AND NOT APPLE)
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
endif()
find_package(miniz QUIET)
@@ -173,7 +174,9 @@ target_include_directories(issngpu PRIVATE shim shadps4 ${SHADER_INCLUDE} third_
target_compile_definitions(issngpu PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
target_compile_options(issngpu PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against issn-probe.
if (NOT WIN32)
if (APPLE)
target_link_options(issngpu PRIVATE -Wl,-undefined,dynamic_lookup)
elseif (NOT WIN32)
target_link_options(issngpu PRIVATE -Wl,--allow-shlib-undefined)
endif()
# glslang's own CMake package: SPIRV and the default resource limits are separate targets in
@@ -193,6 +196,8 @@ if (WIN32)
target_compile_definitions(issngpu PUBLIC NOMINMAX WIN32_LEAN_AND_MEAN)
target_compile_options(issngpu PRIVATE -include ${CMAKE_CURRENT_SOURCE_DIR}/shim/issn_win_compat.h)
target_link_libraries(issngpu PUBLIC Vulkan::Vulkan ws2_32 bcrypt ntdll synchronization winmm pdh)
elseif (APPLE)
target_link_libraries(issngpu PUBLIC Vulkan::Vulkan)
else()
target_link_libraries(issngpu PUBLIC PkgConfig::X11)
endif()
+14 -2
View File
@@ -3,6 +3,9 @@
#pragma once
#include <cstdint>
#include <type_traits>
#include <atomic>
#include <cstdlib>
#include <cstring>
@@ -780,7 +783,11 @@ struct PM4CmdWriteData {
template <typename T>
T Address() const {
return reinterpret_cast<T>(addr64);
if constexpr (std::is_integral_v<T>) {
return static_cast<T>(addr64);
} else {
return reinterpret_cast<T>(static_cast<uintptr_t>(addr64));
}
}
};
@@ -811,7 +818,12 @@ struct PM4CmdEventWriteEos {
template <typename T = u32*>
T Address() const {
return reinterpret_cast<T>(address_lo | u64(address_hi) << 32);
const u64 address = address_lo | u64(address_hi) << 32;
if constexpr (std::is_integral_v<T>) {
return static_cast<T>(address);
} else {
return reinterpret_cast<T>(static_cast<uintptr_t>(address));
}
}
u32 DataDWord() const {
@@ -634,7 +634,7 @@ static std::pair<VAddr, u64> WidenReadFault(const Buffer& buffer, VAddr device_a
return {device_addr, size};
}
const VAddr begin = std::max(Common::AlignDown(device_addr, window), buffer.CpuAddr());
const VAddr end = std::min(Common::AlignUp(device_addr + size, window),
const VAddr end = std::min<VAddr>(Common::AlignUp(device_addr + size, window),
buffer.CpuAddr() + buffer.SizeBytes());
return {begin, end - begin};
}
@@ -1699,7 +1699,7 @@ bool BufferCache::DownloadWindowMultiBuffer(VAddr window_addr, u64 window_size)
}
seen.push_back(id);
const VAddr lo = std::max(window_addr, buffer.CpuAddr());
const VAddr hi = std::min(window_addr + window_size, buffer.CpuAddr() + buffer.SizeBytes());
const VAddr hi = std::min<VAddr>(window_addr + window_size, buffer.CpuAddr() + buffer.SizeBytes());
if (hi <= lo) {
return;
}
@@ -2148,7 +2148,7 @@ BufferCache::FaultTicket BufferCache::PrepareFaultDownload(VAddr device_addr, u6
seen.push_back(id);
const VAddr lo = std::max(addr, window_buffer.CpuAddr());
const VAddr hi =
std::min(addr + bytes, window_buffer.CpuAddr() + window_buffer.SizeBytes());
std::min<VAddr>(addr + bytes, window_buffer.CpuAddr() + window_buffer.SizeBytes());
if (hi <= lo) {
return;
}
@@ -3258,7 +3258,7 @@ u64 BufferCache::NewestAscWriter(std::span<const FaultCopy> copies, u64 done_bel
VAddr clo = ~VAddr{0}, chi = 0; // q11e: the copies' bounding box
for (const FaultCopy& copy : copies) {
clo = std::min(clo, copy.addr);
chi = std::max(chi, copy.addr + copy.size);
chi = std::max<VAddr>(chi, copy.addr + copy.size);
}
for (u64 v = top; v != 0 && v > done_below && top - v < asc_write_hist.size(); --v) {
const size_t i = v % asc_write_hist.size();
+8
View File
@@ -21,7 +21,11 @@
#ifndef _WIN64
#include <sys/mman.h>
#ifdef __APPLE__
#include <sys/ucontext.h>
#else
#include <ucontext.h>
#endif
#include "common/adaptive_mutex.h"
#ifdef ENABLE_USERFAULTFD
#include <thread>
@@ -212,7 +216,11 @@ static bool ReadFaultRipEnabled() {
static void SnapshotReadFaultPage(VAddr) {}
static void LogReadFaultRip(void*, VAddr) {}
static u64 FaultRip(void* context) {
#ifdef __APPLE__
return static_cast<u64>(static_cast<ucontext_t*>(context)->uc_mcontext->__ss.__rip);
#else
return static_cast<u64>(static_cast<ucontext_t*>(context)->uc_mcontext.gregs[REG_RIP]);
#endif
}
#endif
@@ -284,6 +284,11 @@ bool Instance::CreateDevice() {
return false;
};
#ifdef __APPLE__
// port: MoltenVK is a portability implementation, which must have this enabled when it
// offers it (the name is a beta extension's, so spelled out).
add_extension("VK_KHR_portability_subset");
#endif
// Required
ASSERT_MSG(add_extension(VK_KHR_SWAPCHAIN_EXTENSION_NAME),
"Required Vulkan extension unavailable: {}", VK_KHR_SWAPCHAIN_EXTENSION_NAME);
@@ -41,6 +41,7 @@
#include "video_core/renderer_vulkan/vk_pipeline_compile_pool.h"
#include "video_core/renderer_vulkan/vk_pipeline_serialization.h"
#include "video_core/renderer_vulkan/vk_shader_util.h"
#include <immintrin.h> // _mm_pause
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/renderer_vulkan/vk_shader_util.h"
@@ -222,6 +222,11 @@ std::vector<const char*> GetInstanceExtensions(Frontend::WindowSystemType window
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
#ifdef __APPLE__
// port: the loader only lists MoltenVK (a portability driver) when the instance asks for it.
extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
#endif
// SHADPS4_DLSS=1: Streamline starts here, before vkCreateInstance, and adds what it needs.
Dlss::AddInstanceExtensions(extensions);
// issn-axy: ISSN_XESS=1 - libxess loads here and adds what it needs (issn_xess.h).
@@ -424,6 +429,9 @@ vk::UniqueInstance CreateInstance(Frontend::WindowSystemType window_type, bool e
vk::StructureChain<vk::InstanceCreateInfo, vk::LayerSettingsCreateInfoEXT> instance_ci_chain = {
vk::InstanceCreateInfo{
#ifdef __APPLE__
.flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
#endif
.pApplicationInfo = &application_info,
.enabledLayerCount = static_cast<u32>(layers.size()),
.ppEnabledLayerNames = layers.data(),
+12
View File
@@ -46,6 +46,18 @@ asm(".section .rdata,\"dr\"\n"
".global issn_font_ttf_end\n"
"issn_font_ttf_end:\n"
".text\n");
#elif defined(__APPLE__)
// Mach-O: C symbols carry a leading underscore, read-only data is __TEXT,__const.
asm(".section __TEXT,__const\n"
".p2align 4\n"
".private_extern _issn_font_ttf\n"
".globl _issn_font_ttf\n"
"_issn_font_ttf:\n"
".incbin \"" ISSN_FONT_PATH "\"\n"
".private_extern _issn_font_ttf_end\n"
".globl _issn_font_ttf_end\n"
"_issn_font_ttf_end:\n"
".text\n");
#else
asm(".section .rodata\n"
".balign 16\n"
+51 -1
View File
@@ -7,7 +7,11 @@
#include <vector>
#include "video_core/renderer_vulkan/vk_instance.h"
#ifndef _WIN32
#ifdef __APPLE__
#include <mach/mach.h>
#include <sys/resource.h>
#include <sys/sysctl.h>
#elif !defined(_WIN32)
#include <cstring>
#include <dlfcn.h>
#include <sys/sysinfo.h>
@@ -123,6 +127,52 @@ void Run(const Vulkan::Instance* instance) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
#elif defined(__APPLE__)
// port: macOS measurements. CPU from getrusage, RAM from the Mach task and hw.memsize, VRAM from
// VK_EXT_memory_budget (MoltenVK); GPU load stays "n/a".
uint64_t ProcessMicros() { // user + system time of the whole process
rusage r{};
if (getrusage(RUSAGE_SELF, &r) != 0) return 0;
return uint64_t(r.ru_utime.tv_sec + r.ru_stime.tv_sec) * 1000000u +
uint64_t(r.ru_utime.tv_usec + r.ru_stime.tv_usec);
}
void Run(const Vulkan::Instance* instance) {
const unsigned cores = std::max(1u, std::thread::hardware_concurrency());
constexpr double GiB = 1024.0 * 1024.0 * 1024.0;
uint64_t ram_total = 0;
size_t length = sizeof(ram_total);
if (sysctlbyname("hw.memsize", &ram_total, &length, nullptr, 0) != 0) ram_total = 0;
uint64_t last_process = 0;
auto last_wall = std::chrono::steady_clock::now();
while (true) {
Sample s;
s.gpu_name = std::string(instance->GetModelName());
const uint64_t process = ProcessMicros();
const auto wall = std::chrono::steady_clock::now();
if (last_process) {
const uint64_t wall_us = uint64_t(std::chrono::duration<double, std::micro>(wall - last_wall).count());
s.cpu_percent = CpuPercent(process - last_process, wall_us, cores);
}
last_process = process;
last_wall = wall;
mach_task_basic_info_data_t task{};
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&task), &count) == KERN_SUCCESS)
s.ram_used_gb = double(task.resident_size) / GiB;
if (ram_total) s.ram_total_gb = double(ram_total) / GiB;
if (instance->CanReportMemoryUsage()) {
s.vram_used_gb = double(instance->GetDeviceMemoryUsage()) / GiB;
s.vram_total_gb = double(instance->GetTotalMemoryBudget()) / GiB;
}
s.gpu_percent = -1.0f;
{
std::scoped_lock lock{mutex};
latest = std::move(s);
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
#else
// port: Linux measurements. CPU and RAM from /proc, VRAM from VK_EXT_memory_budget, GPU load from
// NVML (NVIDIA, loaded at run time) or the amdgpu sysfs counter; anything else stays "n/a".
+14
View File
@@ -10,6 +10,9 @@
#ifdef _WIN32
#include <bit>
#include <windows.h>
#elif defined(__APPLE__)
#include <pthread.h>
#include <sys/qos.h>
#else
#include <sched.h>
#include <sys/resource.h>
@@ -31,6 +34,17 @@ inline unsigned Available() {
inline void MakeBackground() {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE);
}
#elif defined(__APPLE__)
/// Hardware threads available to the process (macOS has no affinity masks).
inline unsigned Available() {
return std::max(1u, std::thread::hardware_concurrency());
}
/// The calling thread runs at background QoS: on Apple silicon, the efficiency cores, so
/// it never takes time from the game's threads.
inline void MakeBackground() {
pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0);
}
#else
/// Hardware threads available to the process.
+15
View File
@@ -9,6 +9,10 @@
#ifdef _WIN32
#include <windows.h>
#undef near
#elif defined(__APPLE__)
#include <pthread.h>
#include <sys/ucontext.h>
#include <unistd.h>
#else
#include <ucontext.h>
#include <unistd.h>
@@ -61,7 +65,15 @@ void Note(std::uint64_t address, const void* data, std::uint64_t size, Source so
}
void Record(std::uint64_t address, const void* data, std::uint64_t size, Source source) {
#ifdef __APPLE__
static thread_local const std::uint32_t tid = [] {
std::uint64_t id = 0;
pthread_threadid_np(nullptr, &id);
return static_cast<std::uint32_t>(id);
}();
#else
static thread_local const std::uint32_t tid = static_cast<std::uint32_t>(gettid());
#endif
Entry e{address, size, 0, __rdtsc(), source, tid};
std::memcpy(&e.first, data, size < 8 ? size : 8);
Push(ring, head, e);
@@ -93,6 +105,9 @@ extern "C" void issngpu_dump_guest_writes(void* ucontext) {
// The loader passes EXCEPTION_POINTERS (see signal_context.cpp).
const CONTEXT* c = static_cast<const EXCEPTION_POINTERS*>(ucontext)->ContextRecord;
const std::uint64_t regs[] = {c->Rax, c->Rbx, c->Rcx, c->Rdx, c->Rsi, c->Rdi, c->R14, c->R15};
#elif defined(__APPLE__)
const auto& s = static_cast<const ucontext_t*>(ucontext)->uc_mcontext->__ss;
const std::uint64_t regs[] = {s.__rax, s.__rbx, s.__rcx, s.__rdx, s.__rsi, s.__rdi, s.__r14, s.__r15};
#else
const auto* uc = static_cast<const ucontext_t*>(ucontext);
const auto* g = uc->uc_mcontext.gregs;
+9
View File
@@ -5,6 +5,9 @@
#include <cstring>
#include <string>
#include <SDL3/SDL.h>
#ifdef __APPLE__
#include <SDL3/SDL_metal.h>
#endif
#ifdef _WIN32
#include <windows.h>
#endif
@@ -116,6 +119,12 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
window_info.type = WindowSystemType::Windows;
window_info.render_surface = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
} else
#elif defined(__APPLE__)
// port: MoltenVK draws into the CAMetalLayer of an SDL Metal view.
if (driver && !std::strcmp(driver, "cocoa")) {
window_info.type = WindowSystemType::Metal;
window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window));
} else
#endif
if (driver && !std::strcmp(driver, "x11")) {
window_info.type = WindowSystemType::X11;
+35
View File
@@ -0,0 +1,35 @@
issn on macOS (Apple silicon) - TEST BUILD
==========================================
This is an early test build. The port compiles and its checks pass on an M4
(Vulkan through MoltenVK: command submission + readback PASS; CPU: complete
but SSE4A, which the port emulates as on Intel). It has NOT yet been run with
the game: every report helps.
What you need
- A Mac with Apple silicon, macOS 13 or newer, Rosetta 2
(softwareupdate --install-rosetta).
- Python 3 (python3; macOS offers to install it on first use).
- Your own dump of the game, prepared as described in the main README.
This package contains no game files.
Running
1. Unzip, open Terminal in the folder, and run: bash run.sh
(the same arguments and environment variables as on Linux).
2. macOS may refuse the unsigned binaries the first time: run
xattr -dr com.apple.quarantine . in the folder, then try again.
How it works on a Mac
- The build is x86_64 and runs under Rosetta 2: the game's own x86-64 code
runs as on a PC. issn-probe restarts itself with ROSETTA_ADVERTISE_AVX=1,
which Rosetta needs before it reports AVX/AVX2/F16C/BMI in CPUID.
- Graphics: Vulkan through MoltenVK (bin/libMoltenVK.dylib) onto Metal.
- Memory: the guest packs pointers into 40 bits, so the whole process heap
must stay below 1 TiB. A patched mimalloc (packaging/macos) keeps it in
32-64 GiB instead of macOS's usual ~128 TiB.
Reporting
Please send: the Mac model, the macOS version, what happened, and the whole
terminal output (and out/*.log if there are any).
Building it yourself: see packaging/macos/README.md.
+35
View File
@@ -0,0 +1,35 @@
# issn on macOS
The port builds on macOS as an **x86_64** program that Rosetta 2 runs on Apple silicon, with
Vulkan through MoltenVK. It is not a GPTK/Wine build: the same sources as on Linux, with a
`__APPLE__` branch where the systems differ. Status: builds, `issn-probe --vulkan-only` and
`--cpu-features` pass on an M4; not yet run with the game.
## Building
The repository path must not contain spaces (FFmpeg's configure does not accept them).
```sh
brew install cmake ninja pkg-config boost # host tools; Boost is header-only here
bash packaging/macos/build-deps-x86_64.sh # x86_64 dependencies into out/macos-deps (once)
bash build.sh # out/issn-probe, out/gpu/libissngpu.dylib
bash packaging/macos/package.sh # optional: dist/issn-macos-x86_64.zip for testers
```
`ISSN_MAC_DEPS` points both scripts at another dependency prefix. The arm64 Homebrew is never
linked: only Boost's headers are taken from it.
## What differs from Linux
| Linux | macOS |
|---|---|
| guest TLS: `arch_prctl(ARCH_SET_GS)` | a pthread key, read as `gs:[8*key]`; the `fs:` sites are rewritten as on Windows (`patch_tcb_loads`) |
| host heap below 1 TiB: non-PIE brk heap, `mallopt` | mimalloc 3.5.3 patched to map only in 32-64 GiB (`mimalloc-os-window.patch`), linked first |
| `ucontext` `gregs` | `uc_mcontext->__ss` |
| `process_vm_readv`, `/proc/self/task` | `mach_vm_read_overwrite`, `task_threads` |
| `memfd_create` | `shm_open` + immediate unlink |
| timed mutex/rwlock waits, `CLOCK_MONOTONIC` waits | try + short sleep (`compat_mac.c`); `CLOCK_REALTIME` (as on Windows) |
| `SCHED_IDLE` helpers | `QOS_CLASS_BACKGROUND` |
| X11/Wayland surface | SDL Metal view, `VK_EXT_metal_surface` |
| — | `VK_KHR_portability_enumeration` + `VK_KHR_portability_subset` (MoltenVK) |
| — | Rosetta reports AVX in CPUID only with `ROSETTA_ADVERTISE_AVX=1`: `issn-probe` restarts itself with it |
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# Dependências x86_64 (Intel, rodam no M4 via Rosetta 2) para o port issn_pc no macOS.
# Instala tudo em out/macos-deps (ou ISSN_MAC_DEPS) sem tocar no Homebrew ARM (/opt/homebrew).
# Idempotente: cada etapa concluída deixa um carimbo em $P/.stamps e é pulada na próxima vez.
# O caminho do repositório não pode ter espaços (o configure do FFmpeg não aceita).
set -euo pipefail
D="$(cd -- "$(dirname -- "$0")" && pwd)" # packaging/macos (o patch do mimalloc)
ROOT="$(cd -- "$D/../.." && pwd)"
P="${ISSN_MAC_DEPS:-$ROOT/out/macos-deps}" # prefixo de instalação (x86_64)
S="$ROOT/out/macos-deps-src" # códigos-fonte
mkdir -p "$P/.stamps" "$S"
export MACOSX_DEPLOYMENT_TARGET=13.0
ARCH="-arch x86_64 -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET"
JOBS=$(sysctl -n hw.ncpu)
CMAKE_COMMON=(-G Ninja -DCMAKE_BUILD_TYPE=Release
-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET
-DCMAKE_INSTALL_PREFIX="$P" -DCMAKE_PREFIX_PATH="$P"
-DCMAKE_IGNORE_PREFIX_PATH=/opt/homebrew # nunca pegar biblioteca ARM do Homebrew
-DCMAKE_POLICY_VERSION_MINIMUM=3.5)
step() { echo; echo "=================== $1 ($(date +%H:%M:%S))"; }
done_() { touch "$P/.stamps/$1"; echo "OK: $1"; }
have() { [[ -f "$P/.stamps/$1" ]]; }
fetch() { # fetch <dir> <url> <tag> [--recursive]
[[ -d "$S/$1/.git" ]] || git clone --depth 1 --branch "$3" ${4:-} "$2" "$S/$1" -q
}
cm() { # cm <name> <srcdir> [cmake args...]
local name=$1 src=$2; shift 2
cmake -S "$src" -B "$S/build-$name" "${CMAKE_COMMON[@]}" "$@" > "$S/build-$name.cmake.log"
cmake --build "$S/build-$name" -j "$JOBS" > "$S/build-$name.build.log"
cmake --install "$S/build-$name" > "$S/build-$name.install.log"
}
if ! have vulkan-headers; then step "Vulkan-Headers"
fetch Vulkan-Headers https://github.com/KhronosGroup/Vulkan-Headers.git vulkan-sdk-1.4.363.0
cm vulkan-headers "$S/Vulkan-Headers"; done_ vulkan-headers; fi
if ! have vulkan-loader; then step "Vulkan-Loader"
fetch Vulkan-Loader https://github.com/KhronosGroup/Vulkan-Loader.git vulkan-sdk-1.4.363.0
cm vulkan-loader "$S/Vulkan-Loader" -DVULKAN_HEADERS_INSTALL_DIR="$P" -DBUILD_TESTS=OFF
done_ vulkan-loader; fi
if ! have moltenvk; then step "MoltenVK v1.4.2 (binário universal oficial da Khronos)"
mkdir -p "$S/moltenvk" && cd "$S/moltenvk"
[[ -f MoltenVK-macos.tar ]] || curl -sSL -o MoltenVK-macos.tar \
https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.2/MoltenVK-macos.tar
rm -rf x && mkdir x && tar -xf MoltenVK-macos.tar -C x
dylib=$(find x -name libMoltenVK.dylib -path '*macOS*' | head -1)
[[ -n $dylib ]] || dylib=$(find x -name libMoltenVK.dylib | head -1)
lipo -info "$dylib"
mkdir -p "$P/lib" "$P/share/vulkan/icd.d"
cp "$dylib" "$P/lib/libMoltenVK.dylib"
printf '{\n "file_format_version": "1.0.0",\n "ICD": {\n "library_path": "../../../lib/libMoltenVK.dylib",\n "api_version": "1.4.0",\n "is_portability_driver": true\n }\n}\n' \
> "$P/share/vulkan/icd.d/MoltenVK_icd.json"
cd "$D"; done_ moltenvk; fi
if ! have vma; then step "VulkanMemoryAllocator (só cabeçalho)"
fetch VMA https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git v3.5.0
cm vma "$S/VMA"; done_ vma; fi
if ! have robin-map; then step "tsl-robin-map (só cabeçalho)"
fetch robin-map https://github.com/Tessil/robin-map.git v1.4.1
cm robin-map "$S/robin-map"; done_ robin-map; fi
if ! have fmt; then step "fmt 12.2.0"
fetch fmt https://github.com/fmtlib/fmt.git 12.2.0
cm fmt "$S/fmt" -DFMT_TEST=OFF -DFMT_DOC=OFF; done_ fmt; fi
if ! have xxhash; then step "xxHash 0.8.4"
fetch xxHash https://github.com/Cyan4973/xxHash.git v0.8.4
make -C "$S/xxHash" -j"$JOBS" CC="clang $ARCH" PREFIX="$P" lib > "$S/build-xxhash.log"
make -C "$S/xxHash" CC="clang $ARCH" PREFIX="$P" install >> "$S/build-xxhash.log"
done_ xxhash; fi
if ! have zydis; then step "Zydis 4.1.1"
fetch zydis https://github.com/zyantific/zydis.git v4.1.1 --recursive
cm zydis "$S/zydis" -DZYDIS_BUILD_TOOLS=OFF -DZYDIS_BUILD_EXAMPLES=OFF -DZYDIS_BUILD_SHARED_LIB=OFF
done_ zydis; fi
if ! have zycore; then step "Zycore (a dependência do Zydis, instalada à parte para o zydis-config)"
cm zycore "$S/zydis/dependencies/zycore" -DZYCORE_BUILD_SHARED_LIB=OFF
done_ zycore; fi
if ! have mimalloc; then step "mimalloc 3.5.3 com janela de endereços abaixo de 1 TiB"
# O guest do PS4 guarda ponteiros em 40 bits; o malloc do macOS fica perto de 128 TiB.
# O patch (mimalloc-os-window.patch) põe toda memória do processo em 32–64 GiB.
fetch mimalloc https://github.com/microsoft/mimalloc.git v3.5.3
git -C "$S/mimalloc" apply --reverse --check "$D/mimalloc-os-window.patch" 2>/dev/null ||
git -C "$S/mimalloc" apply "$D/mimalloc-os-window.patch"
cm mimalloc "$S/mimalloc" -DMI_OVERRIDE=ON -DMI_OSX_ZONE=ON -DMI_OSX_INTERPOSE=ON \
-DMI_BUILD_SHARED=OFF -DMI_BUILD_STATIC=ON -DMI_BUILD_OBJECT=ON -DMI_BUILD_TESTS=OFF \
"-DCMAKE_C_FLAGS=-DMI_OS_WINDOW_START=0x800000000ULL -DMI_OS_WINDOW_END=0x1000000000ULL"
done_ mimalloc; fi
if ! have sdl3; then step "SDL 3.4.18"
fetch SDL https://github.com/libsdl-org/SDL.git release-3.4.18
cm sdl3 "$S/SDL" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF -DSDL_EXAMPLES=OFF
done_ sdl3; fi
if ! have glslang; then step "glslang 16.6.0"
fetch glslang https://github.com/KhronosGroup/glslang.git 16.6.0
cm glslang "$S/glslang" -DENABLE_OPT=OFF -DGLSLANG_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \
-DENABLE_GLSLANG_BINARIES=ON -DGLSLANG_ENABLE_INSTALL=ON
done_ glslang; fi
if ! have ffmpeg; then step "FFmpeg 9.0.2 (só as bibliotecas que o port usa)"
fetch FFmpeg https://github.com/FFmpeg/FFmpeg.git n9.0.2
cd "$S/FFmpeg"
./configure --prefix="$P" --arch=x86_64 --target-os=darwin --enable-cross-compile \
--cc="clang $ARCH" --cxx="clang++ $ARCH" --nm=nm --ar=ar --ranlib=ranlib \
--enable-shared --disable-static --disable-programs --disable-doc \
--disable-autodetect --disable-network --disable-avdevice --disable-avfilter \
> "$S/build-ffmpeg.configure.log"
make -j"$JOBS" > "$S/build-ffmpeg.build.log" 2>&1
make install > "$S/build-ffmpeg.install.log"
cd "$D"; done_ ffmpeg; fi
step "Verificação"
for f in "$P"/lib/*.dylib; do [[ -L $f ]] || printf '%-34s %s\n' "$(basename "$f")" "$(lipo -archs "$f")"; done
echo "pkg-config:"; PKG_CONFIG_LIBDIR="$P/lib/pkgconfig:$P/share/pkgconfig" pkg-config --modversion sdl3 libxxhash libavcodec vulkan 2>&1
echo; echo "TODAS AS DEPENDÊNCIAS PRONTAS ($(date +%H:%M:%S))"
+38
View File
@@ -0,0 +1,38 @@
diff --git a/src/prim/unix/prim.c b/src/prim/unix/prim.c
index 25c2ade..d5fa76c 100644
--- a/src/prim/unix/prim.c
+++ b/src/prim/unix/prim.c
@@ -305,7 +305,33 @@ static int unix_madvise(void* addr, size_t size, int advice) {
#endif
}
+#if defined(MI_OS_WINDOW_START) && defined(MI_OS_WINDOW_END)
+// Optional address window: every non-fixed OS allocation is placed in
+// [MI_OS_WINDOW_START, MI_OS_WINDOW_END). issn_pc builds mimalloc with a window below 1 TiB on
+// macOS, where the system heap lives near 128 TiB: heap pointers then stay small, like a Linux
+// brk heap (its PS4 guest code packs pointers into 40 bits).
+static uintptr_t mi_os_window_next = MI_OS_WINDOW_START;
+static void* unix_mmap_window(size_t size, int protect_flags, int flags, int fd) {
+ const uintptr_t align = (uintptr_t)64 << 20; // 64 MiB covers the arena and page alignments
+ for (;;) {
+ uintptr_t at = __atomic_load_n(&mi_os_window_next, __ATOMIC_RELAXED);
+ const uintptr_t start = (at + align - 1) & ~(align - 1);
+ if (start + size > (uintptr_t)MI_OS_WINDOW_END || start + size < start) { errno = ENOMEM; return MAP_FAILED; }
+ if (!__atomic_compare_exchange_n(&mi_os_window_next, &at, start + size, true, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) continue;
+ void* p = mmap((void*)start, size, protect_flags, flags, fd, 0);
+ if (p == (void*)start) return p;
+ if (p != MAP_FAILED) munmap(p, size); // that range is taken: the kernel placed it elsewhere
+ }
+}
+#endif
+
static void* unix_mmap_prim(void* addr, size_t size, int protect_flags, int flags, int fd) {
+ #if defined(MI_OS_WINDOW_START) && defined(MI_OS_WINDOW_END)
+ if ((flags & MAP_FIXED) == 0 &&
+ ((uintptr_t)addr < (uintptr_t)MI_OS_WINDOW_START || (uintptr_t)addr >= (uintptr_t)MI_OS_WINDOW_END)) {
+ return unix_mmap_window(size, protect_flags, flags, fd);
+ }
+ #endif
void* p = mmap(addr, size, protect_flags, flags, fd, 0 /* offset */);
#if defined(__linux__) && defined(PR_SET_VMA)
if (p!=MAP_FAILED && p!=NULL) {
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Builds dist/issn-macos-x86_64.zip: the port's current macOS build (run build.sh first), its
# scripts and every non-system library it loads, with relative install names, so a tester
# needs neither Homebrew nor the dependency build. Unzip, then: bash run.sh (ISSN_PREBUILT=1).
set -euo pipefail
cd -- "$(dirname -- "$0")/../.."
deps=${ISSN_MAC_DEPS:-$PWD/out/macos-deps}
[[ -f out/issn-probe && -f out/gpu/libissngpu.dylib ]] || { echo 'Build first: bash build.sh' >&2; exit 1; }
name=issn-macos-x86_64
stage=dist/$name
rm -rf "$stage" "dist/$name.zip" && mkdir -p "$stage/bin/gpu"
cp out/issn-probe out/issn-gpu-capabilities "$stage/bin/"
cp out/gpu/libissngpu.dylib "$stage/bin/gpu/"
cp "$deps/lib/libMoltenVK.dylib" "$stage/bin/"
# shadPS4 points the Vulkan loader at the executable's directory; the path is the file's own.
printf '{"file_format_version":"1.0.0","ICD":{"library_path":"./libMoltenVK.dylib","api_version":"1.4.0","is_portability_driver":true}}\n' \
> "$stage/bin/MoltenVK_icd.json"
# The libraries of $deps a Mach-O file loads, by install name (absolute or @rpath/).
deps_of() {
otool -L "$1" | tail -n +2 | awk '{print $1}' | while read -r dep; do
case $dep in
"$deps"/lib/*|@rpath/*) [[ $dep == */libissngpu.dylib ]] || echo "$dep" ;;
esac
done
}
# Copies them into bin/ until nothing new appears (FFmpeg's libraries load each other).
queue=("$stage/bin/issn-probe" "$stage/bin/issn-gpu-capabilities" "$stage/bin/gpu/libissngpu.dylib")
while ((${#queue[@]})); do
file=${queue[0]} queue=("${queue[@]:1}")
for dep in $(deps_of "$file"); do
base=$(basename "$dep")
if [[ ! -f $stage/bin/$base ]]; then
cp -L "$deps/lib/$base" "$stage/bin/$base"
chmod u+w "$stage/bin/$base"
install_name_tool -id "@rpath/$base" "$stage/bin/$base" 2>/dev/null
queue+=("$stage/bin/$base")
fi
install_name_tool -change "$dep" "@rpath/$base" "$file" 2>/dev/null
done
done
# Search paths: bin/ for everything, never the build machine's directories.
for file in "$stage"/bin/issn-probe "$stage"/bin/issn-gpu-capabilities "$stage"/bin/gpu/libissngpu.dylib "$stage"/bin/*.dylib; do
for path in $(otool -l "$file" | awk '/LC_RPATH/{getline; getline; print $2}'); do
[[ $path == @* ]] || install_name_tool -delete_rpath "$path" "$file" 2>/dev/null
done
done
install_name_tool -add_rpath @loader_path "$stage/bin/issn-probe" 2>/dev/null || true
install_name_tool -add_rpath @loader_path "$stage/bin/issn-gpu-capabilities" 2>/dev/null || true
install_name_tool -add_rpath @loader_path/.. "$stage/bin/gpu/libissngpu.dylib" 2>/dev/null || true
install_name_tool -add_rpath @loader_path "$stage/bin/gpu/libissngpu.dylib" 2>/dev/null || true
# install_name_tool invalidates signatures; Apple silicon (Rosetta included) needs one: ad hoc.
for file in "$stage"/bin/issn-probe "$stage"/bin/issn-gpu-capabilities "$stage"/bin/gpu/*.dylib "$stage"/bin/*.dylib; do
codesign --force --sign - "$file" 2>/dev/null
done
cp -R run.sh run.py scripts launcher patches LICENSE "$stage/"
cp packaging/macos/README-macOS.txt "$stage/"
# run.sh on a packaged build: nothing is built (ISSN_PREBUILT).
sed -i '' 's/^data=\${ISSN_DATA_DIR:-.}$/data=${ISSN_DATA_DIR:-.}; export ISSN_PREBUILT=${ISSN_PREBUILT:-1}/' "$stage/run.sh"
(cd dist && ditto -c -k --keepParent "$name" "$name.zip")
ls -lh "dist/$name.zip"
+83
View File
@@ -0,0 +1,83 @@
/* macOS implementations of compat_mac.h. */
#ifdef __APPLE__
#include "compat_mac.h"
#include <fcntl.h>
#include <limits.h>
#include <mach-o/dyld.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/sysctl.h>
#include <unistd.h>
static int expired(const struct timespec *deadline) {
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
return now.tv_sec > deadline->tv_sec || (now.tv_sec == deadline->tv_sec && now.tv_nsec >= deadline->tv_nsec);
}
/* Polls the try variant with a short sleep: the guest's timed waits are rare and short, and
* a deadline in the past still gets one attempt (POSIX). */
static int timed_try(int (*attempt)(void *), void *object, const struct timespec *deadline) {
if (deadline->tv_nsec < 0 || deadline->tv_nsec >= 1000000000) return EINVAL;
for (;;) {
int e = attempt(object);
if (e != EBUSY) return e;
if (expired(deadline)) return ETIMEDOUT;
struct timespec nap = {0, 200000};
nanosleep(&nap, NULL);
}
}
static int try_mutex(void *m) { return pthread_mutex_trylock(m); }
static int try_read(void *l) { return pthread_rwlock_tryrdlock(l); }
static int try_write(void *l) { return pthread_rwlock_trywrlock(l); }
int compat_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *deadline) {
return timed_try(try_mutex, mutex, deadline);
}
int compat_rwlock_timedrdlock(pthread_rwlock_t *lock, const struct timespec *deadline) {
return timed_try(try_read, lock, deadline);
}
int compat_rwlock_timedwrlock(pthread_rwlock_t *lock, const struct timespec *deadline) {
return timed_try(try_write, lock, deadline);
}
int compat_memfd_create(const char *name, unsigned flags) {
(void)name;
static unsigned counter;
char path[32]; /* PSHMNAMLEN is 31 */
for (int attempt = 0; attempt < 100; ++attempt) {
snprintf(path, sizeof(path), "/issn.%d.%u", (int)getpid(),
__atomic_fetch_add(&counter, 1, __ATOMIC_RELAXED));
int fd = shm_open(path, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd >= 0) {
shm_unlink(path);
if (flags & MFD_CLOEXEC) fcntl(fd, F_SETFD, FD_CLOEXEC);
return fd;
}
if (errno != EEXIST) return -1;
}
return -1;
}
void compat_close_from(int first) {
struct rlimit limit;
int last = 65536;
if (!getrlimit(RLIMIT_NOFILE, &limit) && limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur < (rlim_t)last)
last = (int)limit.rlim_cur;
for (int fd = first; fd < last; ++fd) close(fd);
}
void compat_rosetta_advertise_avx(char **argv) {
int translated = 0;
size_t size = sizeof translated;
if (sysctlbyname("sysctl.proc_translated", &translated, &size, NULL, 0) || !translated) return;
if (getenv("ROSETTA_ADVERTISE_AVX")) return; /* already set, or chosen by the user */
char path[PATH_MAX];
uint32_t length = sizeof path;
if (_NSGetExecutablePath(path, &length)) return;
setenv("ROSETTA_ADVERTISE_AVX", "1", 1);
execv(path, argv);
unsetenv("ROSETTA_ADVERTISE_AVX"); /* execv failed: carry on, cpu_check reports AVX */
}
#endif
+69
View File
@@ -0,0 +1,69 @@
/* macOS (Apple clang, x86_64 under Rosetta 2) counterparts of the Linux calls the runtime
* uses. Included through runtime.h after the C library headers, like compat_win.h, so the
* macros below only replace calls in this project's own sources. <pthread.h> and <time.h>
* come first so later includes of them cannot see the macros. */
#ifndef ISSN_COMPAT_MAC_H
#define ISSN_COMPAT_MAC_H
#ifdef __APPLE__
#include <stdint.h>
#include <time.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
/* struct stat's nanosecond times are st_*timespec on macOS (st_*tim on Linux). */
#define st_atim st_atimespec
#define st_mtim st_mtimespec
#define st_ctim st_ctimespec
#ifndef CLOCK_REALTIME_COARSE
#define CLOCK_REALTIME_COARSE CLOCK_REALTIME
#endif
#ifndef PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
#define PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP PTHREAD_RECURSIVE_MUTEX_INITIALIZER
#endif
/* Kernel thread id (Linux gettid): the 64-bit id macOS gives each thread. */
static inline pid_t compat_gettid(void) {
uint64_t id = 0;
pthread_threadid_np(NULL, &id);
return (pid_t)id;
}
#define gettid compat_gettid
/* Only the calling thread can name itself on macOS. */
#define pthread_setname_np(thread, name) ((void)(thread), pthread_setname_np(name))
/* macOS condition variables wait on CLOCK_REALTIME only (no pthread_condattr_setclock), as
* with winpthreads: runtime_sema.c and runtime_eventflag.c pick WAIT_CLOCK = CLOCK_REALTIME. */
#define pthread_condattr_setclock(attr, clock) ((void)(attr), (clock) == CLOCK_REALTIME ? 0 : EINVAL)
/* No timed mutex/rwlock acquisition on macOS: try until the CLOCK_REALTIME deadline. */
int compat_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *deadline);
int compat_rwlock_timedrdlock(pthread_rwlock_t *lock, const struct timespec *deadline);
int compat_rwlock_timedwrlock(pthread_rwlock_t *lock, const struct timespec *deadline);
#define pthread_mutex_timedlock compat_mutex_timedlock
#define pthread_rwlock_timedrdlock compat_rwlock_timedrdlock
#define pthread_rwlock_timedwrlock compat_rwlock_timedwrlock
/* Guest memory backing (Linux memfd_create): an unlinked POSIX shared memory object. */
#define MFD_CLOEXEC 1u
int compat_memfd_create(const char *name, unsigned flags);
#define memfd_create compat_memfd_create
/* Returning pages to the pool (Linux punch hole) has no shared-memory counterpart: released
* pages stay committed until the guest reuses them. */
#define FALLOC_FL_KEEP_SIZE 1
#define FALLOC_FL_PUNCH_HOLE 2
#define fallocate(fd, mode, offset, length) ((void)(fd), (void)(mode), (void)(offset), (void)(length), 0)
/* Linux close_range(first, ~0u, 0): closes every descriptor from first up. */
void compat_close_from(int first);
/* Rosetta 2 runs AVX/AVX2 but only reports them in CPUID to a process started with
* ROSETTA_ADVERTISE_AVX=1: a translated process without it starts itself again with it. */
void compat_rosetta_advertise_avx(char **argv);
#endif
#endif
+91 -20
View File
@@ -23,14 +23,24 @@
#include <signal.h>
#else
#include <sys/mman.h>
#ifdef __APPLE__
#include <malloc/malloc.h>
#include <sys/ucontext.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
/* <mach/vm_param.h> defines round_page/trunc_page macros; this file has its own round_page. */
#undef round_page
#undef trunc_page
#else
#include <malloc.h>
#include <ucontext.h>
#include <sys/syscall.h>
#endif
#include <unistd.h>
#include <signal.h>
#include <ucontext.h>
#include <fcntl.h>
#include <dlfcn.h>
#include <execinfo.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#endif
@@ -111,7 +121,7 @@ static ABI __attribute__((noreturn)) void unresolved(uint32_t id, uintptr_t argu
uintptr_t caller=(uintptr_t)__builtin_return_address(0)-(uintptr_t)image;
for (uint64_t m=0;m<module_count;++m)
if (caller>=modules[m].base && caller-modules[m].base<modules[m].size)
printf("Caller in linked module %" PRIu64 " (%s): +0x%" PRIxPTR "\n",m,m==0 ? "libc.prx" : "system module",caller-modules[m].base);
printf("Caller in linked module %" PRIu64 " (%s): +0x%" PRIxPTR "\n",m,m==0 ? "libc.prx" : "system module",(uintptr_t)(caller-modules[m].base));
runtime_report();
puts(entered_game ? "Original guest entry instructions executed; game initialization is incomplete." :
"Native libc initialization is incomplete; game entry has not run.");
@@ -121,7 +131,12 @@ static ABI __attribute__((noreturn)) void unresolved(uint32_t id, uintptr_t argu
#ifndef _WIN32
/* enter_on_stack(entry, arg0, arg1, stack_top): call entry(arg0,arg1) on a new stack. */
void enter_on_stack(void *entry, void *arg0, void *arg1, void *top);
__asm__(".text\n.globl enter_on_stack\nenter_on_stack:\n"
#ifdef __APPLE__
#define ENTER_ON_STACK "_enter_on_stack" /* Mach-O symbols carry a leading underscore */
#else
#define ENTER_ON_STACK "enter_on_stack"
#endif
__asm__(".text\n.globl " ENTER_ON_STACK "\n" ENTER_ON_STACK ":\n"
" push %rbp\n mov %rsp,%rbp\n and $-16,%rcx\n mov %rcx,%rsp\n"
" mov %rdi,%rax\n mov %rsi,%rdi\n mov %rdx,%rsi\n call *%rax\n"
" mov %rbp,%rsp\n pop %rbp\n ret\n");
@@ -272,8 +287,11 @@ static LONG CALLBACK vectored_fault(EXCEPTION_POINTERS *info) {
return EXCEPTION_CONTINUE_SEARCH;
}
static LONG WINAPI unhandled_fault(EXCEPTION_POINTERS *info) { return fatal_exception(info); }
#endif
#if defined(_WIN32) || defined(__APPLE__)
/* Guest code reads its TCB with `mov rax, gs:[0]` (link_*.py rewrote fs:[0]); on Windows
* the displacement becomes the TEB TLS slot that holds the guest TCB (runtime_thread.c). */
* the displacement becomes the TEB TLS slot that holds the guest TCB, on macOS the pthread
* key's slot in the thread's TSD array (runtime_thread.c). */
uint32_t runtime_thread_tcb_offset(void);
/* `mov r64, gs:[0]` (link_modules.py rewrote the fs: of each `mov r64, fs:[0]`): 65, REX.W[R],
* 8B, ModRM mod=00 rm=100 (any register), SIB 25 (disp32 only), displacement 0. issn: any
@@ -296,21 +314,39 @@ static uint64_t patch_tcb_loads(const Segment *segments, uint64_t count) {
}
#endif
#ifndef _WIN32
/* ucontext gregs of IsaRegs.gpr (x86 numbering). */
/* Saved registers of a signal context, in IsaRegs.gpr's x86 numbering: Linux keeps them in
* uc_mcontext.gregs[REG_*], macOS in the x86_thread_state64 that uc_mcontext points to. */
#ifdef __APPLE__
typedef __uint64_t ctx_reg_t;
static ctx_reg_t *ctx_gpr(ucontext_t *uc, int index) {
_STRUCT_X86_THREAD_STATE64 *s = &uc->uc_mcontext->__ss;
ctx_reg_t *const r[16] = {&s->__rax, &s->__rcx, &s->__rdx, &s->__rbx, &s->__rsp, &s->__rbp, &s->__rsi, &s->__rdi,
&s->__r8, &s->__r9, &s->__r10, &s->__r11, &s->__r12, &s->__r13, &s->__r14, &s->__r15};
return r[index];
}
#define CTX_RIP(uc) ((uc)->uc_mcontext->__ss.__rip)
#define CTX_RFLAGS(uc) ((uc)->uc_mcontext->__ss.__rflags)
#else
typedef greg_t ctx_reg_t;
static const int isa_gregs[16]={REG_RAX,REG_RCX,REG_RDX,REG_RBX,REG_RSP,REG_RBP,REG_RSI,REG_RDI,
REG_R8,REG_R9,REG_R10,REG_R11,REG_R12,REG_R13,REG_R14,REG_R15};
static ctx_reg_t *ctx_gpr(ucontext_t *uc, int index) { return &uc->uc_mcontext.gregs[isa_gregs[index]]; }
#define CTX_RIP(uc) ((uc)->uc_mcontext.gregs[REG_RIP])
#define CTX_RFLAGS(uc) ((uc)->uc_mcontext.gregs[REG_EFL])
#endif
enum { CTX_RBP = 5, CTX_RDI = 7 }; /* ctx_gpr indices */
static void fault(int sig, siginfo_t *info, void *context) {
if (sig == SIGTRAP && isa_table) {
/* An int3 ISA site (RIP is past the CC byte); any other trap ends in the report below. */
greg_t *g = ((ucontext_t *)context)->uc_mcontext.gregs;
ucontext_t *trap = context;
IsaRegs regs;
uintptr_t rip;
for (int i = 0; i < 16; ++i) regs.gpr[i] = (uint64_t)g[isa_gregs[i]];
regs.rflags = (uint64_t)g[REG_EFL];
if (isa_rewrite_trap(isa_table, (uintptr_t)g[REG_RIP] - 1, &regs, &rip)) {
for (int i = 0; i < 16; ++i) g[isa_gregs[i]] = (greg_t)regs.gpr[i];
g[REG_EFL] = (greg_t)regs.rflags;
g[REG_RIP] = (greg_t)rip;
for (int i = 0; i < 16; ++i) regs.gpr[i] = (uint64_t)*ctx_gpr(trap, i);
regs.rflags = (uint64_t)CTX_RFLAGS(trap);
if (isa_rewrite_trap(isa_table, (uintptr_t)CTX_RIP(trap) - 1, &regs, &rip)) {
for (int i = 0; i < 16; ++i) *ctx_gpr(trap, i) = (ctx_reg_t)regs.gpr[i];
CTX_RFLAGS(trap) = (ctx_reg_t)regs.rflags;
CTX_RIP(trap) = (ctx_reg_t)rip;
return;
}
}
@@ -328,7 +364,7 @@ static void fault(int sig, siginfo_t *info, void *context) {
}
/* The process is terminating: dladdr/snprintf are acceptable here. */
ucontext_t *uc = context;
uintptr_t rip = (uintptr_t)uc->uc_mcontext.gregs[REG_RIP];
uintptr_t rip = (uintptr_t)CTX_RIP(uc);
char line[512];
Dl_info where;
int site = isa_table ? isa_rewrite_site_of_stub(isa_table, rip) : -1;
@@ -389,20 +425,30 @@ static void write_hex(char *out, uint64_t v) {
const char digits[] = "0123456789abcdef";
for (int i = 0; i < 16; ++i) out[i] = digits[(v >> (60 - i * 4)) & 15];
}
/* Reads a frame record without faulting on a bad rbp. */
static int read_frame(uintptr_t frame[2], uintptr_t rbp) {
#ifdef __APPLE__
mach_vm_size_t got=0;
return mach_vm_read_overwrite(mach_task_self(),(mach_vm_address_t)rbp,2*sizeof(uintptr_t),
(mach_vm_address_t)frame,&got)==KERN_SUCCESS && got==2*sizeof(uintptr_t);
#else
struct iovec local={frame,2*sizeof(uintptr_t)}, remote={(void *)rbp,2*sizeof(uintptr_t)};
return process_vm_readv(getpid(),&local,1,&remote,1,0)==(ssize_t)(2*sizeof(uintptr_t));
#endif
}
static void dump_frames(ucontext_t *uc) {
char line[] = " tid=0000000000000000 rip=0000000000000000 image-relative=0000000000000000 host-relative=0000000000000000\n";
uintptr_t rip=(uintptr_t)uc->uc_mcontext.gregs[REG_RIP], rbp=(uintptr_t)uc->uc_mcontext.gregs[REG_RBP];
uintptr_t rip=(uintptr_t)CTX_RIP(uc), rbp=(uintptr_t)*ctx_gpr(uc,CTX_RBP);
uint64_t tid=(uint64_t)gettid();
/* First argument register: the lock address when a thread waits on a futex. */
char arg[]=" tid=0000000000000000 rdi=0000000000000000\n";
write_hex(arg+6,tid); write_hex(arg+27,(uint64_t)uc->uc_mcontext.gregs[REG_RDI]);
write_hex(arg+6,tid); write_hex(arg+27,(uint64_t)*ctx_gpr(uc,CTX_RDI));
{ ssize_t written_=write(2,arg,sizeof(arg)-1); (void)written_; }
for (int depth=0; depth<24; ++depth) {
write_hex(line+6,tid); write_hex(line+27,rip); write_hex(line+59,rip-(uintptr_t)image); write_hex(line+90,rip-exe_base);
{ ssize_t written_=write(2,line,sizeof(line)-1); (void)written_; }
uintptr_t frame[2];
struct iovec local={frame,sizeof(frame)}, remote={(void *)rbp,sizeof(frame)};
if (!rbp || process_vm_readv(getpid(),&local,1,&remote,1,0)!=(ssize_t)sizeof(frame)) break;
if (!rbp || !read_frame(frame,rbp)) break;
if (frame[0]<=rbp) break;
rbp=frame[0]; rip=frame[1];
}
@@ -413,6 +459,17 @@ static void watchdog(int sig, siginfo_t *info, void *context) {
const char head[]="STOP: watchdog timeout; thread stacks:\n";
{ ssize_t written_=write(2,head,sizeof(head)-1); (void)written_; }
dump_frames(context);
#ifdef __APPLE__
/* Every other thread dumps its own stack (SIGUSR2): enumerated through the Mach task. */
thread_act_array_t list;
mach_msg_type_number_t count=0;
mach_port_t self=mach_thread_self();
if (task_threads(mach_task_self(),&list,&count)==KERN_SUCCESS)
for (mach_msg_type_number_t i=0; i<count; ++i) {
pthread_t thread=pthread_from_mach_thread_np(list[i]);
if (thread && list[i]!=self) { pthread_kill(thread,SIGUSR2); usleep(20000); }
}
#else
int dir=open("/proc/self/task",O_RDONLY|O_DIRECTORY);
char buffer[4096];
long n;
@@ -424,6 +481,7 @@ static void watchdog(int sig, siginfo_t *info, void *context) {
if (tid>0 && tid!=self) { syscall(SYS_tgkill,getpid(),tid,SIGUSR2); usleep(20000); }
at+=d->reclen;
}
#endif
usleep(100000);
watchdog_extra();
_exit(128 + sig);
@@ -840,7 +898,11 @@ void runtime_restart(void) {
TerminateProcess(GetCurrentProcess(),0);
#else
const char *command = getenv("ISSN_RESTART_COMMAND");
#ifdef __APPLE__
compat_close_from(3);
#else
syscall(SYS_close_range, 3u, ~0u, 0u);
#endif
if (command && *command) {
puts("Runtime: restarting through ISSN_RESTART_COMMAND");
execl("/bin/sh", "sh", "-c", command, (char *)NULL);
@@ -872,11 +934,16 @@ static int cpu_supported(char *missing, size_t size) {
int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IONBF, 0);
#ifndef _WIN32
#if !defined(_WIN32) && !defined(__APPLE__)
/* Keep host heap objects handed to the guest (thread handles, TLS) in the
non-PIE brk heap, i.e. below 1 TiB: the guest packs pointers into 40 bits. */
non-PIE brk heap, i.e. below 1 TiB: the guest packs pointers into 40 bits.
macOS has no brk heap: the build links mimalloc with an address window below
1 TiB instead (packaging/macos). */
mallopt(M_ARENA_MAX,1);
mallopt(M_MMAP_THRESHOLD,32*1024*1024);
#endif
#ifdef __APPLE__
compat_rosetta_advertise_avx(argv);
#endif
if (argc == 2 && !strcmp(argv[1], "--vulkan-only")) return vulkan_smoke();
char missing[128];
@@ -896,6 +963,8 @@ int main(int argc, char **argv) {
* TCB needs one of the 64 TEB TLS slots before DLLs (Vulkan drivers) take them. */
runtime_memory_reserve();
runtime_thread_tcb_offset();
#elif defined(__APPLE__)
runtime_thread_tcb_offset(); /* the pthread key that holds the guest TCB (runtime_thread.c) */
#endif
int cpu_only = 0, strict_imports = 0, check_imports = 0;
unsigned timeout_seconds = 10;
@@ -1206,6 +1275,8 @@ int main(int argc, char **argv) {
if (gpu_enabled) issngpu_apply_game_patches();
#ifdef _WIN32
printf("Guest TCB loads redirected to the TEB TLS slot: %" PRIu64 "\n", patch_tcb_loads(segments, ns));
#elif defined(__APPLE__)
printf("Guest TCB loads redirected to the pthread TSD slot: %" PRIu64 "\n", patch_tcb_loads(segments, ns));
#endif
if (isa_table) {
IsaReport r = isa_rewrite_apply(isa_table, image, stubs, isa_sets);
+3
View File
@@ -10,6 +10,9 @@ typedef issn_jmp_buf sigjmp_buf;
#define sigsetjmp(buffer, save) issn_setjmp(buffer)
#else
#include <setjmp.h>
#ifdef __APPLE__
#include "compat_mac.h"
#endif
#endif
/* Recovery point for speculative guest memory reads on this thread (probe.c fault handler). */
extern __thread sigjmp_buf *runtime_fault_recover;
+7
View File
@@ -55,6 +55,13 @@ static void sleep_until(uint64_t deadline) {
#ifdef _WIN32
/* winpthreads' clock_nanosleep only takes CLOCK_REALTIME (EINVAL otherwise). */
for (uint64_t now=now_ns(); now<deadline; now=now_ns()) compat_sleep_ns(deadline-now);
#elif defined(__APPLE__)
/* No clock_nanosleep on macOS: sleep what is left of the CLOCK_MONOTONIC deadline. */
for (uint64_t now=now_ns(); now<deadline; now=now_ns()) {
const uint64_t left=deadline-now;
struct timespec t={(time_t)(left/1000000000u),(long)(left%1000000000u)};
nanosleep(&t,NULL);
}
#else
struct timespec t={(time_t)(deadline/1000000000u),(long)(deadline%1000000000u)};
while (clock_nanosleep(CLOCK_MONOTONIC,TIMER_ABSTIME,&t,NULL)) {}
+2 -2
View File
@@ -34,8 +34,8 @@ static Flag *flags;
static uint64_t next_id=1;
static size_t created, waits, timeouts;
/* As runtime_sema.c: winpthreads condition variables accept only CLOCK_REALTIME. */
#ifdef _WIN32
/* As runtime_sema.c: winpthreads and macOS condition variables accept only CLOCK_REALTIME. */
#if defined(_WIN32) || defined(__APPLE__)
#define WAIT_CLOCK CLOCK_REALTIME
#else
#define WAIT_CLOCK CLOCK_MONOTONIC
+23
View File
@@ -12,6 +12,11 @@
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#ifdef __APPLE__
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/thread_act.h>
#endif
#include <x86intrin.h>
#ifndef _WIN32
#include <sys/random.h>
@@ -245,6 +250,22 @@ static ABI int32_t guest_getrusage(int who,GuestRusage *out) {
compat_cpu_times(who,&user,&kernel);
out->utime=(GuestTimeval){user/1000000,user%1000000};
out->stime=(GuestTimeval){kernel/1000000,kernel%1000000};
#elif defined(__APPLE__)
if (who==0) {
struct rusage r;
getrusage(RUSAGE_SELF,&r);
out->utime=(GuestTimeval){r.ru_utime.tv_sec,r.ru_utime.tv_usec};
out->stime=(GuestTimeval){r.ru_stime.tv_sec,r.ru_stime.tv_usec};
} else { /* no RUSAGE_THREAD: the calling thread's times come from Mach */
thread_basic_info_data_t info;
mach_msg_type_number_t count=THREAD_BASIC_INFO_COUNT;
mach_port_t self=mach_thread_self();
if (thread_info(self,THREAD_BASIC_INFO,(thread_info_t)&info,&count)==KERN_SUCCESS) {
out->utime=(GuestTimeval){info.user_time.seconds,info.user_time.microseconds};
out->stime=(GuestTimeval){info.system_time.seconds,info.system_time.microseconds};
}
mach_port_deallocate(mach_task_self(),self);
}
#else
struct rusage r;
getrusage(who==0 ? RUSAGE_SELF : RUSAGE_THREAD,&r);
@@ -260,6 +281,8 @@ static ABI int32_t guest_sysctl(const int32_t *name,uint32_t namelen,void *old,u
if (!old || !oldlen) return fail_posix(EINVAL);
#ifdef _WIN32
if (compat_random(old,(size_t)*oldlen)) return fail_posix(EIO);
#elif defined(__APPLE__)
arc4random_buf(old,(size_t)*oldlen);
#else
if (getrandom(old,(size_t)*oldlen,0)<0) return fail_posix(errno);
#endif
+6
View File
@@ -762,7 +762,13 @@ void *runtime_low_map(size_t size, int prot) {
#else
void *p=MAP_FAILED;
while (low_next+size<=USER_MIN) {
#ifdef __APPLE__
/* No MAP_FIXED_NOREPLACE: map with the address as a hint and keep it only there. */
p=mmap((void *)low_next,size,prot,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
if (p!=MAP_FAILED && p!=(void *)low_next) { munmap(p,size); p=MAP_FAILED; }
#else
p=mmap((void *)low_next,size,prot,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE,-1,0);
#endif
low_next+=size+PAGE; /* unmapped gap catches overruns */
if (p!=MAP_FAILED) break;
}
+3 -2
View File
@@ -35,8 +35,9 @@ static void host_check(int e) {
if (e) { fprintf(stderr,"STOP: semaphore host error %d\n",e); exit(21); }
}
/* Waits use this clock for their deadlines. winpthreads condition variables accept only
* CLOCK_REALTIME (pthread_condattr_setclock(CLOCK_MONOTONIC) fails with EINVAL). */
#ifdef _WIN32
* CLOCK_REALTIME (pthread_condattr_setclock(CLOCK_MONOTONIC) fails with EINVAL), and macOS
* has no pthread_condattr_setclock at all. */
#if defined(_WIN32) || defined(__APPLE__)
#define WAIT_CLOCK CLOCK_REALTIME
#else
#define WAIT_CLOCK CLOCK_MONOTONIC
+22 -1
View File
@@ -4,7 +4,9 @@
* keeps FS. Priorities/affinity are recorded, not enforced by a PS4 scheduler.
* Windows: GS is the TEB and cannot be moved. The guest TCB lives in a TLS slot of the TEB
* instead, and the loader rewrites the load to `mov rax, gs:[0x1480+8*slot]` (TlsSlots),
* the same 9-byte instruction with another displacement. */
* the same 9-byte instruction with another displacement.
* macOS (x86_64 under Rosetta 2): GS is the thread's TSD array and cannot be moved either.
* The guest TCB lives in a pthread key, which macOS stores at gs:[8*key]: same rewrite. */
#define _GNU_SOURCE
#include "runtime.h"
#include <stdio.h>
@@ -17,6 +19,8 @@
#include <unistd.h>
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <sys/mman.h>
#else
#include <sys/syscall.h>
#include <sys/mman.h>
@@ -85,6 +89,23 @@ static void set_gs(void *base) {
if (!TlsSetValue(tls_slot,base)) { fprintf(stderr,"STOP: TlsSetValue failed (%lu)\n",GetLastError()); exit(21); }
}
#define tls_block_alloc(size) _aligned_malloc(size,64)
#elif defined(__APPLE__)
/* pthread_getspecific(key) is `mov rax, gs:[8*key]` on x86_64 macOS: the guest's TCB load
* reads the key directly. */
static pthread_key_t tcb_key;
static pthread_once_t tcb_key_once=PTHREAD_ONCE_INIT;
static void create_tcb_key(void) {
if (pthread_key_create(&tcb_key,NULL)) { fputs("STOP: pthread_key_create failed\n",stderr); exit(21); }
}
uint32_t runtime_thread_tcb_offset(void) {
pthread_once(&tcb_key_once,create_tcb_key);
return 8*(uint32_t)tcb_key;
}
static void set_gs(void *base) {
runtime_thread_tcb_offset();
if (pthread_setspecific(tcb_key,base)) { fputs("STOP: pthread_setspecific failed\n",stderr); exit(21); }
}
#define tls_block_alloc(size) aligned_alloc(64,size)
#else
static void set_gs(void *base) {
if (syscall(SYS_arch_prctl,ARCH_SET_GS,(unsigned long)base)) { perror("STOP: arch_prctl(ARCH_SET_GS)"); exit(21); }
+17
View File
@@ -3,12 +3,29 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef __APPLE__
#include <limits.h>
#include <mach-o/dyld.h>
#include <string.h>
#endif
#define CHECK(call) do { VkResult r = (call); if (r != VK_SUCCESS) { \
fprintf(stderr, "Vulkan: %s returned %d\n", #call, r); exit(1); } } while (0)
int vulkan_smoke(void) {
VkApplicationInfo app = {.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName="inFAMOUS Second Son native probe", .apiVersion=VK_API_VERSION_1_0};
VkInstanceCreateInfo ici = {.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo=&app};
#ifdef __APPLE__
/* As the GPU library does: the MoltenVK ICD file sits next to the executable. */
char path[PATH_MAX]; uint32_t length = sizeof path;
if (!getenv("VK_DRIVER_FILES") && !_NSGetExecutablePath(path, &length)) {
char *slash = strrchr(path, '/');
if (slash) { *slash = 0; setenv("VK_DRIVER_FILES", path, 1); }
}
/* MoltenVK is a portability driver: the loader only lists it when asked to. */
const char *portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME;
ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
ici.enabledExtensionCount = 1; ici.ppEnabledExtensionNames = &portability;
#endif
VkInstance instance; CHECK(vkCreateInstance(&ici, NULL, &instance));
uint32_t count = 0; CHECK(vkEnumeratePhysicalDevices(instance, &count, NULL));
if (!count) { fprintf(stderr, "Vulkan: no physical devices\n"); vkDestroyInstance(instance, NULL); return 1; }
+23
View File
@@ -4,6 +4,10 @@
#include <stdlib.h>
#include <string.h>
#include <vulkan/vulkan.h>
#ifdef __APPLE__
#include <limits.h>
#include <mach-o/dyld.h>
#endif
#include "video_core/amdgpu/issn_msaa_default.h"
@@ -73,8 +77,27 @@ int main(int argc, char **argv) {
.pApplicationName = "issn scene scaling probe",
.apiVersion = VK_API_VERSION_1_3,
};
#ifdef __APPLE__
/* As the GPU library does: the MoltenVK ICD file sits next to the executable, and the
loader only lists a portability driver when the instance asks for it. */
char path[PATH_MAX];
uint32_t length = sizeof path;
if (!getenv("VK_DRIVER_FILES") && !_NSGetExecutablePath(path, &length)) {
char *slash = strrchr(path, '/');
if (slash) {
*slash = 0;
setenv("VK_DRIVER_FILES", path, 1);
}
}
const char *portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME;
#endif
const VkInstanceCreateInfo create = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
#ifdef __APPLE__
.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR,
.enabledExtensionCount = 1,
.ppEnabledExtensionNames = &portability,
#endif
.pApplicationInfo = &app,
};
VkInstance instance = VK_NULL_HANDLE;