mirror of
https://github.com/Mrsuss60/bloodborne_pc_windows_port.git
synced 2026-11-07 13:56:36 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5d2f2dd4 | ||
|
|
7fd13ca336 |
No files matched your search
+10
-2
@@ -6,14 +6,15 @@ bbport.ini
|
||||
fsr4_shaders/
|
||||
pgo/
|
||||
*.png
|
||||
last_run.log
|
||||
*.log
|
||||
launcher.log
|
||||
launcher_settings.json
|
||||
dist/
|
||||
packaging/runtime-paths.nix
|
||||
fsr4_411/
|
||||
vkd3d-proton.cache
|
||||
tools/fsr4cap/*.exe
|
||||
perf.data
|
||||
# Local leftovers: patch backups, generated SPIR-V, analysis outputs, test saves and routes
|
||||
*.orig
|
||||
/*.spv
|
||||
/1060/
|
||||
@@ -23,3 +24,10 @@ perf.data
|
||||
/bloodborne_upscalers_*.md
|
||||
mods.json
|
||||
patches.json
|
||||
*.zip
|
||||
FORK/
|
||||
EBOOT/
|
||||
scratch/
|
||||
test_th.*
|
||||
.vscode/
|
||||
.idea/
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
# Bloodborne PC - Windows Native Port Improvements & Changes
|
||||
|
||||
This document details the optimizations, fixes, and refactoring applied to ensure stability, smooth frame pacing, low CPU utilization, and clear diagnostics on 4-core / 4-thread systems (such as the Intel Core i5-4590, 14 GB DDR3, GTX 1650 4 GB) while dynamically adapting to other hardware configurations (AMD GPUs, hybrid P/E CPUs, variable RAM/VRAM, and audio configurations). It also documents features and stability improvements ported from Linux upstream 0.3.
|
||||
|
||||
---
|
||||
|
||||
## 1. Crash Analysis & Resolutions (Wolf Enemy Combat & Stability)
|
||||
|
||||
### A. Dynamic VRAM GC Thresholds (`gpu/shadps4/video_core/texture_cache/texture_cache.cpp`)
|
||||
- **Root Cause**: On 4 GB GPUs like the GTX 1650, texture cache garbage collection was set to trigger critical eviction only at 97% of VRAM. When the player entered combat with the first wolf enemy, dynamic particle effects, blood decals, and new texture allocations pushed VRAM beyond 100%, triggering `vk::Result=ErrorOutOfDeviceMemory STOP: GPU library assertion failed`.
|
||||
- **Change**: Configured safe, progressive dynamic GC thresholds (70% trigger, 80% pressure, 88% critical eviction), fully configurable via environment variables (`BB_GC_TRIGGER`, `BB_GC_PRESSURE`, `BB_GC_CRITICAL`). This ensures stale images are evicted smoothly before GPU memory is exhausted.
|
||||
- **Presenter Resilience** (`gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp`): Added fallback allocation without `WITHIN_BUDGET` flag and host memory fallback in `RecreateFrame` during transient memory pressure.
|
||||
|
||||
### B. Remnant Slice Remapping in Win32 Memory Views (`src/win32_memory.c`, `src/runtime_memory.c`)
|
||||
- **Root Cause**: Windows memory virtualization using `VirtualAlloc2` placeholders and `MapViewOfFile3` was dropping entire 64 KiB section views when guest code partially unmapped an allocation, failing to preserve and remap remnant prefix and suffix slices.
|
||||
- **Change**: Implemented view slice tracking and automatic remnant remapping with `restore_remnant()`. Added a 64-entry lock-free ring buffer tracking recent memory operations (`g_mem_op_ring`), dumped during crash diagnosis.
|
||||
|
||||
### C. Removal of Silent Null Skips in VEH (`src/probe.c`)
|
||||
- **Root Cause**: The Vectored Exception Handler had silent instruction skips (`8b 04 07`, `8b 3c 19`, `c5 fa 5c`) that forced destination registers to zero. This masked upstream null pointer bugs and cascaded into all general-purpose registers (RAX, RBX, RCX, RDX, RDI, RBP) becoming 0, culminating in the crash at guest offset `0x28ce9b5`.
|
||||
- **Change**: Removed silent skips. If an unhandled memory fault occurs, a full crash dump (`out/crash_<timestamp>.log`) is generated with registers, stack frames, RBP chains, and faulting thread import history before clean termination.
|
||||
|
||||
### D. Pipeline Cache SRT Walker Fault Handler (`gpu/shadps4/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp`)
|
||||
- **Root Cause**: Restored pipeline cache entries invoked SRT walkers without ensuring the signal handler was registered, leading to access violation crashes.
|
||||
- **Change**: Added `EnsureSrtFaultHandler()` on walker registration and wrapped concurrent instruction patching in a mutex (`patch_mutex`) to support parallel draw-preparation workers.
|
||||
|
||||
### E. AvPlayer Thread Safety (`gpu/shim/core/libraries/kernel/threads.h`)
|
||||
- **Root Cause**: AvPlayer demuxer and decoder threads attempted to join themselves or collided with owner destructors during playback teardown.
|
||||
- **Change**: Implemented thread-safe `Take()`/`Finish()` pattern under `std::mutex` ensuring clean detachment and non-reentrant joining.
|
||||
|
||||
### F. High-Entropy ASLR & Address Space Collision (Error 487) (`gpu/CMakeLists.txt`, `src/win32_memory.c`, `src/probe.c`, `src/runtime_memory.c`)
|
||||
- **Root Cause**: Windows 64-bit High-Entropy ASLR and DynamicBase were enabled by default on `bbport.exe`. Windows randomized the main thread stack and CRT heap into `[0x800000000, 0xfc00000000)` (~1 TB guest address space). When `VirtualAlloc2` attempted to reserve the full 1 TB placeholder, it collided with the preexisting thread stack and returned `Windows error 487 (ERROR_INVALID_ADDRESS)`. As a result, `space()` failed, `runtime_low_map` could not allocate, image loading fell back to host allocations exceeding 40 bits (`1<<40`), and guest thread stack allocations failed with `"Cannot allocate guest thread"`.
|
||||
- **Change**:
|
||||
1. Linked `bbport.exe` with `-Wl,--disable-dynamicbase` and `-Wl,--disable-high-entropy-va` in `gpu/CMakeLists.txt`, ensuring bottom-up memory layout below 32 GB.
|
||||
2. Called `runtime_memory_reserve_space()` at the very start of `main()` in `src/probe.c` before Vulkan, driver DLLs, or background worker threads are initialized.
|
||||
3. Added a chunked placeholder fallback in `win_mem_space()`: if a single 1 TB placeholder fails due to any preexisting system or hook allocation, it queries memory and reserves all free regions in `[start, end)` as placeholders, setting `space_start` and `space_end` successfully.
|
||||
|
||||
---
|
||||
|
||||
## 2. Audio Subsystem Refactoring (Distortion & Stuttering Fixes)
|
||||
|
||||
### A. Eliminated High-Priority Spinloop (`src/runtime_audio.c`)
|
||||
- **Root Cause**: `output_port` ran a tight busy-wait loop (`__builtin_ia32_pause()`) while the thread was set to `THREAD_PRIORITY_TIME_CRITICAL`. On a 4C/4T CPU, this locked one core at 100% and preempted FMOD mixer and audio worker threads, causing severe underruns, robotic voice, and distorted sound.
|
||||
- **Change**: Replaced spinning `sleep_until` with `host_sleep_until_ns` waitable timers and demoted audio thread priority to `THREAD_PRIORITY_HIGHEST`.
|
||||
|
||||
### B. Queue Steering & Cadence Alignment (`src/runtime_audio.c`)
|
||||
- **Change**: Restored queue steering against the hardware WASAPI clock. The minimum queue level over a 32-buffer window is tracked and steered towards a 2-period target buffer via `adjust_ns` (±3% cadence corrections), absorbing clock drift between CPU QPC and audio DAC clocks.
|
||||
- **Prefill Headroom**: Added `BB_AUDIO_PREFILL` environment variable support (default 2 periods) for configurable buffer headroom.
|
||||
|
||||
### C. Soft-Knee Saturation Limiter (`src/runtime_audio.c`)
|
||||
- **Root Cause**: 8-channel audio was hard-clipped at `[-1.0, 1.0]`, causing square-wave clipping and harsh buzzing during multi-channel audio peaks.
|
||||
- **Change**: Replaced hard clipping with a smooth soft-knee saturation curve (`soft_clip`), preventing square-wave distortion while preserving audio dynamic range.
|
||||
|
||||
### D. Scratch Buffer in AJM Decoding (`src/runtime_ajm.c`)
|
||||
- **Change**: Replaced per-job heap `malloc`/`free` calls during multi-chunk input decoding with a 4096-byte stack scratch buffer, reducing heap allocation overhead during ATRAC9 audio playback.
|
||||
|
||||
---
|
||||
|
||||
## 3. CPU Performance & Frame Time Smoothing (4C/4T Haswell & Dynamic Hardware Adaptation)
|
||||
|
||||
### A. Native Win32 Synchronization Primitives (`src/host_sync.h`)
|
||||
- **Change**: Replaced winpthreads overhead with native Windows `SRWLOCK` and `CONDITION_VARIABLE` across `runtime_mutex.c`, `runtime_rwlock.c`, `runtime_sema.c`, `runtime_thread.c`, `runtime_kernel.c`, `runtime_savedata.c`, and `runtime_pad.c`.
|
||||
|
||||
### B. High-Resolution Waitable Timers (`src/runtime_host.c`)
|
||||
- **Change**: Implemented waitable timers using `CreateWaitableTimerExW` with `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION` on Windows 10/11, enabling sub-millisecond sleep precision without burning CPU cycles in spinloops.
|
||||
|
||||
### C. Process Scheduling & Power Throttling (`src/runtime_thread.c`)
|
||||
- **Change**: Configured `ABOVE_NORMAL_PRIORITY_CLASS` and explicitly disabled Windows EcoQoS / power throttling via `SetProcessInformation(ProcessPowerThrottling)`, ensuring CPU cores maintain full execution clock frequency.
|
||||
|
||||
### D. Thread Priority Mapping (`src/runtime_thread.c`)
|
||||
- **Change**: Refined PS4 priority translation (`ps4_prio_to_win32`) to cap normal guest worker threads at `THREAD_PRIORITY_HIGHEST`, preventing 60+ guest threads from monopolizing 4 physical CPU cores.
|
||||
|
||||
### E. Dynamic Core Count Detection & `BB_NCPU` (`src/runtime_kernel.c`)
|
||||
- **Root Cause**: `guest_sysctl` hardcoded `hw.ncpu = 7`, misleading the guest engine into scheduling across 7 cores on 4-core systems.
|
||||
- **Change**: Implemented dynamic core detection via `GetSystemInfo` / `sysconf`, with an explicit `BB_NCPU` override environment variable.
|
||||
|
||||
### F. Removed Redundant Syscall Checks (`gpu/shadps4/video_core/renderer_vulkan/vk_bind_helper.h`, `vk_scheduler.h`)
|
||||
- **Change**: Replaced per-draw `std::jthread::joinable()` syscall checks with atomic boolean flags (`running`, `recorder_running`), eliminating repeated OS kernel transitions during render command recording.
|
||||
|
||||
### G. Non-SEH Register Context Restoration (`src/runtime_host.c`, `src/runtime.h`)
|
||||
- **Change**: Implemented `runtime_setjmp`/`runtime_longjmp` restoring all Win64 callee-saved registers without triggering Windows SEH stack unwinding across guest frames lacking xdata unwind metadata.
|
||||
|
||||
---
|
||||
|
||||
## 4. Launcher & Diagnostics (`launcher.py`, `src/probe.c`)
|
||||
|
||||
### A. Pre-Launch Verification Check & Setup Audit (`launcher.py`)
|
||||
- **Feature**: Added comprehensive pre-launch verification (`verify_prelaunch`) and dedicated `✓ Verify Setup` UI button:
|
||||
- Verifies game directory exists and contains a valid, non-empty `eboot.bin` (reporting exact file size in MiB).
|
||||
- Checks for game asset directories (such as `dvdroot_ps4`).
|
||||
- Verifies compiled native binaries (`out/bbport.exe` and `out/bbgpu.dll`), ensuring they have non-zero size and logging their build compilation timestamps.
|
||||
- Verifies runtime launch scripts (`run.bat`, `scripts/patches.py`).
|
||||
- Checks Vulkan runtime driver availability via `vulkan-1.dll`.
|
||||
- Automatically runs before launch and displays detailed diagnostic checklist in the launcher console and GUI dialogs.
|
||||
|
||||
### B. Feature Toggles & Vanilla Mode (`launcher.py`)
|
||||
- **Toggles**: Independent checkboxes for Upscaler (FSR), Object Motion, Overlay Menu, FPS Patch, Mods Folder, Resolution Scaling, Debug Tracing, Watchdog, and Draw Prep Workers.
|
||||
- **Presets**: "Vanilla mode" sets all optional extras off with 30 FPS native lock; "Everything on" restores defaults.
|
||||
|
||||
### C. Enhanced Crash Diagnostics (`src/probe.c`)
|
||||
- **Crash Log**: Automatically writes `out/crash_<timestamp>.log` containing exception code, fault address, guest eboot offset, full x86-64 register state, 16 stack frames with symbol mapping, 16 guest RBP frames, last 32 imports of the faulting thread, and recent memory operations dump.
|
||||
- **Summary Log**: Periodically writes and finalizes `out/summary.log` tracking runtime, flips, frametime percentiles (p95/p99), freeze durations, and audio underrun counters.
|
||||
|
||||
---
|
||||
|
||||
## 5. Save Data Mount & Performance Regression Fixes
|
||||
|
||||
### A. Title Screen "Failed to create save data" Fix (`src/runtime_savedata.c`)
|
||||
- **Root Cause**: `exports[]` in `src/runtime_savedata.c` only matched old `#p#J` hashes. Bloodborne's EBOOT imports `#O#P` NIDs (e.g., `ZkZhskCPXFw#O#P` for `sceSaveDataInitialize`, `32HQAQdwM2o#O#P` for `sceSaveDataMount`). Because of this mismatch, all save data calls resolved to NULL and executed unbound stubs, leaving `/savedata0` unmounted and halting the game at the title screen with *"Failed to create save data. Press OK to try again."*
|
||||
- **Change**: Added all `#O#P` NIDs and plaintext symbol names to `exports[]`. Enhanced `make_dirs()` to handle both Windows backslashes (`\`) and POSIX forward slashes (`/`).
|
||||
|
||||
### B. Framerate Drop (10 FPS Regression) & CPU Spun Loops Fix (`src/runtime_kernel.c`, `src/probe.c`)
|
||||
- **Root Cause**:
|
||||
1. `exports[]` in `src/runtime_kernel.c` lacked Bloodborne's actual NIDs for `sceKernelClockGettime` (`QBi7HCK03hw#p#J`), `sceKernelUsleep` (`1jfXLRVzisc#p#J`), and `sceKernelReadTsc` (`-2IRUCO--PM#p#J`). `usleep` returned 0 without sleeping, spinning CPU cores at 100%.
|
||||
2. In `src/probe.c`, unbound import execution called `printf` and `fflush(stdout)` unconditionally on every single call without checking `g_bb_trace`. With unresolved/unbound imports being invoked in fast loops, this caused over 400,000 unbuffered console I/O writes and flushes, bottlenecking the process and dropping framerate from 30 FPS down to ~8-10 FPS.
|
||||
- **Change**: Bound correct Bloodborne timer/sleep NIDs in `src/runtime_kernel.c`, and strictly gated unbound import logging in `src/probe.c` behind `if (g_bb_trace && count <= 3)` with low call caps.
|
||||
|
||||
---
|
||||
|
||||
## 6. Low-VRAM (4 GB) & Low-Core (4C/4T) Hardware Adaptation
|
||||
|
||||
### A. VRAM Budget Scaling & Thrashing Elimination (`gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp`, `vk_instance.h`, `texture_cache.cpp`)
|
||||
- **Root Cause**:
|
||||
1. On discrete GPUs, `CollectPhysicalMemoryInfo` subtracted a 1 GB system memory reserve from the dedicated VRAM budget regardless of total VRAM size. On a 4 GB card (GTX 1650), this reduced the usable budget from ~3800 MiB to ~2825 MiB.
|
||||
2. In `TextureCache::GarbageCollectImages`, the default trigger threshold was hardcoded to 70% of budget (`2825 * 0.70 = 1977 MiB`), pressure at 80% (2260 MiB), and critical eviction at 88% (2486 MiB).
|
||||
3. Bloodborne's base working set in Iosefka's Clinic is ~2500–2600 MiB. On 8 GB GPUs (like the RX 6600), the 70% threshold is ~4900 MiB, so GC never triggers (0 evictions). On 4 GB GPUs, 2500 MiB immediately exceeded the 88% critical mark, forcing the texture cache into a permanent critical eviction panic.
|
||||
4. Every 5 seconds, 1389 images were evicted and downloaded back across PCIe using `scheduler.Finish()`, stalling the CPU/GPU pipelines and tanking frame rate from 30 FPS down to 10 FPS.
|
||||
5. During combat with the Scourge Beast, particle stream buffers and blood decal textures were evicted while being accessed by guest draw routines, leading to page-watcher invalidation and guest register corruption (`address 0` fault at `0x28ce9b5`).
|
||||
- **Change**:
|
||||
1. Exposed `GetDeviceLocalMemory()` in `Instance` to query physical dedicated VRAM size.
|
||||
2. In `CollectPhysicalMemoryInfo()`, restricted the 1 GB system reserve subtraction to GPUs with > 4 GB VRAM, preserving the full physical VRAM pool for <= 4 GB cards.
|
||||
3. Dynamically scale GC budget on <= 4 GB GPUs up to 88% of physical VRAM (~3600 MiB on 4 GB cards), with dynamic low-VRAM thresholds: trigger at 88% (3168 MiB), pressure at 93% (3348 MiB), and critical at 97% (3492 MiB). This allows Bloodborne's 2500–2600 MiB working set to remain resident without thrashing.
|
||||
4. Protected recently touched images from aggressive eviction by raising `ticks_to_destroy` to 900 ticks (30 seconds) in aggressive mode and 1800 ticks (60 seconds) in pressured mode, preventing active combat encounter assets from being destroyed.
|
||||
5. Throttled synchronous downloads (`gc_downloads`) to at most 1 per frame to eliminate multiple `scheduler.Finish()` pipeline flushes.
|
||||
|
||||
### B. 4C/4T CPU Thread Scheduling & Starvation Resolution (`src/runtime_thread.c`, `src/host_sync.h`)
|
||||
- **Root Cause**:
|
||||
1. `ps4_prio_to_win32` assigned `THREAD_PRIORITY_HIGHEST` to any thread with priority `<= 260`. Bloodborne spawns over 20 background worker threads (5 `GXWorker`, `HavokWorkerThread`, 6 `CSChrThread`, 6 `CSClothThread`, 6 `SpClothVertexUpdate`) with priority `256`.
|
||||
2. On a 12-thread CPU (Ryzen 5600), hardware threads handle these workers easily. On a 4-core / 4-thread CPU (i5-4590), 20 workers running at `THREAD_PRIORITY_HIGHEST` completely preempted and starved the main game loop thread and Vulkan presenter (running at default `THREAD_PRIORITY_NORMAL`).
|
||||
3. `host_yield()` called Win32 `SwitchToThread()`, which yields only to threads on the same physical core; when no other thread is scheduled on that core, it returns immediately without yielding, causing high-CPU spin contention.
|
||||
- **Change**:
|
||||
1. Added dynamic CPU core count detection (`get_host_cores()`). On systems with 4 cores or fewer, worker pool threads (`prio >= 250`) are assigned `THREAD_PRIORITY_NORMAL` (or `BELOW_NORMAL`), while audio threads maintain elevated priority.
|
||||
2. In `runtime_thread_attach_main()`, elevated the main game loop thread to `THREAD_PRIORITY_ABOVE_NORMAL`, preventing background workers from starving the main loop on low-core CPUs.
|
||||
3. In `host_sync.h`, updated `host_yield()` to fall back to `Sleep(0)` when `SwitchToThread()` fails, giving up the time slice to any ready thread across all cores.
|
||||
|
||||
---
|
||||
|
||||
## 7. Combat / Hit Blood Particle Generator Stack Unwind (`src/probe.c`)
|
||||
|
||||
### A. Clean Frame Restoration for Impact Particle Generation (`0x28ce7b0 .. 0x28ceeb8`)
|
||||
- **Root Cause**:
|
||||
1. Function `0x28ce7b0` is the blood splatter/impact vertex stream generator invoked when any enemy lands a hit on the character.
|
||||
2. The function relies on the System V AMD64 128-byte Red Zone (`[rsp - 0x60]`, `[rsp - 0x38]`, etc.). On Windows, memory pressure / write-tracking faults invoke kernel exception dispatch (`KiUserExceptionDispatch`), which allocates 1.3 KB below `RSP`, clobbering these Red Zone local pointers.
|
||||
3. A prior recovery attempted to branch mid-function to `0x28cea00`. Jumping into the middle of the loop skipped stream initialization and cascaded into reading `[rcx + rdx] = [0 + 4] = 0x4` at `0x28cea45`.
|
||||
- **Change**:
|
||||
1. Updated `win_veh_handler` in `src/probe.c`: if any fault occurs within the entire function body (`0x28ce7ba .. 0x28ceeb8`), it safely reads the preserved callee-saved registers from the stack frame (`RBX`, `R12`, `R13`, `R14`, `R15`, `RBP`), pops the return address, restores `RSP = RSP + 0x68`, and returns directly to the caller (`0x2ab485c` / `0x2ac458e`).
|
||||
2. The caller's code immediately completes the hit reaction and frame without crashing, allowing gameplay to proceed smoothly on all enemy hits.
|
||||
---
|
||||
|
||||
## 8. Linux Upstream 0.3 Port to Windows
|
||||
|
||||
### A. Stability: Atomic Label Writes (Guest Fault `0x263b8e7` Fix)
|
||||
- **Root Cause**: `memcpy` copies 4-7 bytes using two overlapping 4-byte stores. On preemption, the second store could land after guest code freed the label block, corrupting the heap free list with a pointer ending in `...00000004` (guest fault `0x263b8e7`).
|
||||
- **Files Modified**: `src/runtime_memory.c`, `gpu/shadps4/video_core/amdgpu/liverpool.cpp`, `gpu/shadps4/video_core/amdgpu/pm4_cmds.h`.
|
||||
- **Change**: Replaced `memcpy` in `runtime_memory_write_backing` with `store_once()` using aligned 64/32/16/8-bit release atomic stores. In `liverpool.cpp`, implemented `StoreLabel()` using `__atomic_store_n(..., __ATOMIC_RELEASE)` in `SignalEop` and `RunEventWriteEos`. In `pm4_cmds.h`, used atomic release stores for `ReleaseMem::SignalFence`.
|
||||
|
||||
### B. Stability: GPU Breadcrumbs & Diagnostics
|
||||
- **Files**: `gpu/shadps4/video_core/renderer_vulkan/vk_breadcrumbs.h`, `vk_breadcrumbs.cpp`, `gpu/bbgpu.h`, `gpu/shim/bbgpu.cpp`, `src/probe.c`.
|
||||
- **Change**: Ported `vk_breadcrumbs` using `VK_AMD_buffer_marker`. Integrated `bbgpu_dump_breadcrumbs(f)` into `dump_hang_snapshot()` and `write_crash_dump()` in `src/probe.c`, recording active render pass and draw information directly to crash dumps.
|
||||
|
||||
### C. Pre-flight Game Check (CUSA03173 v1.09)
|
||||
- **Files**: `scripts/game_check.py`, `launcher.py`.
|
||||
- **Change**: Ported `scripts/game_check.py` and connected `verify_prelaunch()` in `launcher.py`. Validates Game ID (`CUSA03173`) and version (`1.09`), providing explanations for missing or mismatched game files, and supporting bypass via `BB_SKIP_GAME_CHECK=1`.
|
||||
|
||||
### D. Memory / VRAM Collector Base
|
||||
- **Files**: `gpu/shadps4/video_core/texture_cache/image.cpp`, `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp`, `gpu/shadps4/video_core/texture_cache/texture_cache.h`, `texture_cache.cpp`.
|
||||
- **Change**:
|
||||
1. Configured 64 MB VMA blocks via `BB_VMA_BLOCK_MB` (default 64) to prevent fragmented blocks from being held by empty allocations.
|
||||
2. Added dedicated memory allocation (`VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT`) for large images $\ge$ `BB_VMA_DEDICATED_MB` (default 4 MiB).
|
||||
3. Ported upstream 20-second idle texture collector (`BB_GC_IDLE_SECONDS=20`) in `TextureCache::GarbageCollectImages` while preserving the Vulkan budget query (`VK_EXT_memory_budget`) and dynamic thresholds for low-VRAM GPUs.
|
||||
|
||||
### E. Camera Motion Vector Vertical-Axis Fix
|
||||
- **File**: `gpu/shadps4/video_core/host_shaders/camera_motion.comp`.
|
||||
- **Change**: Replaced inverted NDC/UV vertical sign convention (`1.0 - uv.y * 2.0`) with screen-space row mapping (`uv.y * 2.0 - 1.0`), eliminating stair smearing in FSR 4 and stair blur in FSR 3.1 within 0.05 px of GPU per-vertex motion.
|
||||
|
||||
### F. Anisotropic Filtering (`BB_ANISO`)
|
||||
- **Files**: `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h`, `vk_rasterizer.cpp`, `gpu/shim/bbport_toggles.h`, `launcher.py`.
|
||||
- **Change**: Added `Rasterizer::ForceAnisotropy` clamped to `instance.MaxSamplerAnisotropy()`. Integrated an Anisotropic Filtering dropdown into `launcher.py` with dynamic descriptions, respecting Vanilla Mode (`BB_ANISO=0`).
|
||||
|
||||
### G. Patches & High FPS Enhancements
|
||||
- **Files**: `patches/Bloodborne.xml`, `scripts/patches.py`.
|
||||
- **Change**:
|
||||
1. Ported `Sprint Fix (High FPS)` for 90 FPS and uncap modes (switches wall distance detector from per-frame to speed).
|
||||
2. Ported Intel CPU detection (`intel_cpu` / `intel_tonemap_fix`) and automated activation of `Intel Black Tonemap Fix` on Intel CPUs with `BB_INTEL_TONEMAP_FIX` override.
|
||||
|
||||
---
|
||||
|
||||
## 9. 0.1% and 1.0% Frametime Stutter Elimination & Performance Enhancements
|
||||
|
||||
### A. Critical Path Thread Priority Scheduling (`src/runtime_thread.c`)
|
||||
- **Root Cause**: On 4-core / 4-thread CPUs, `ps4_prio_to_win32()` assigned `THREAD_PRIORITY_NORMAL` to all threads with `prio <= 260` (the 12 background worker threads: `EzWorkPool:0..3`, `GXWorker:0..5`, `ClothWorker`, `HavokWorker`), but downgraded threads with `prio <= 700` to `THREAD_PRIORITY_BELOW_NORMAL`. Bloodborne's primary rendering thread (`GXRenderThread`) and main game loop (`MOMainThread`) are created with PS4 default `prio = 700`. As a result, the critical GPU submission thread ran at lower priority than 12 background workers. During enemy encounters and physics simulations, the workers preempted `GXRenderThread`, causing rendering stalls (`== Stall during rendering at flush 1046`), pipeline bubbles, and severe 1% / 0.1% low frametime drops.
|
||||
- **Change**:
|
||||
1. Explicitly elevated critical path rendering and main threads (`GXRenderThread`, `RenderThread`, `Present`, `Flip`, `Display`, `MOMainThread`, `MainThread`) to `THREAD_PRIORITY_ABOVE_NORMAL`.
|
||||
2. Demoted background worker pools (`Worker`, `Pool`, `Work`, `Cloth`, `Havok`, `Job`) to `THREAD_PRIORITY_BELOW_NORMAL` on systems with $\le$ 4 cores, ensuring the GPU command submission pipeline is never starved or preempted.
|
||||
3. Updated `thread_rename()` to dynamically update Win32 thread priority if a thread was created anonymously and renamed after startup.
|
||||
|
||||
### B. Texture Cache Anti-Thrashing & Calibrated VRAM Thresholds (`gpu/shadps4/video_core/texture_cache/texture_cache.cpp`)
|
||||
- **Root Cause**: On 4 GB discrete GPUs (e.g. GTX 1650), Bloodborne's active scene memory hovers around 3.1–3.2 GB. Under the previous calculation, `pressure_gc_memory` was dynamically clamped to 3,163 MiB. Because memory usage hovered around 3,216 MiB, the collector remained permanently in `pressured = true`. In that state, `ticks_to_destroy` was set to 80 ticks (~0.2s / 3 frames), causing any texture temporarily out of camera view to be evicted. In 5 seconds, over 3,600 images were evicted and downloaded synchronously via `DownloadImageMemory(image_id, true)` (calling `scheduler.Finish()` multiple times per frame). On subsequent frames, these same textures had to be re-allocated, staged, and re-uploaded over PCIe, creating recurring 150–300 ms pipeline freezes.
|
||||
- **Change**:
|
||||
1. Recalibrated thresholds on low-VRAM GPUs: `trigger_gc_memory` (~70% / 2,860 MiB) allows the 20-second idle collector (`BB_GC_IDLE_SECONDS=20`) to reclaim stale area textures smoothly; `pressure_gc_memory` (~90% / 3,680 MiB) prevents false emergency triggers during standard rendering; `critical_gc_memory` (~95% / 3,890 MiB) acts as a safety backstop.
|
||||
2. Replaced tick-based age under pressure with real-time second boundaries: pressured eviction only targets textures idle for $\ge$ 3–5 seconds (`pressure_tick`), while critical eviction targets textures idle for $\ge$ 1 second (`critical_tick`). Active view frustum textures are never evicted.
|
||||
3. Rate-limited synchronous GPU downloads (`scheduler.Finish()`) to at most 1 per submission pass and limited deletions per pass (4 normal, 6 pressure, 16 critical), eliminating pipeline stall cascades.
|
||||
@@ -1,239 +1,137 @@
|
||||
# bbport — a native Linux port of Bloodborne
|
||||
# bloodborne_pc_windows_port: Native Windows Port of Bloodborne
|
||||
|
||||
**English** · [Русский](README.ru.md)
|
||||
`bloodborne_pc_windows_port` is a native 64-bit Windows port of the PlayStation 4 executable of *Bloodborne* (CUSA03173, version 1.09).
|
||||
|
||||
bbport runs the original PlayStation 4 executable of *Bloodborne* (CUSA03173, game version
|
||||
1.09) directly on an x86-64 Linux PC. It is not a general emulator. The game's own x86-64 code
|
||||
executes natively; a small runtime written for this one game replaces the PS4 system libraries;
|
||||
the GPU work is translated to Vulkan by a renderer derived from
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
|
||||
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1.
|
||||
This project is a Windows adaptation of the original Linux port (`bbport` by `deadinside28`). It replaces the Linux-specific kernel, memory mapping, and POSIX threading implementation with a native Win32 runtime, allowing the game to run directly on Windows with Vulkan.
|
||||
|
||||
> **No game files are included.** You need your own dump of Bloodborne (CUSA03173, v1.09).
|
||||
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
|
||||
The game's x86-64 code executes natively on your CPU without general emulation. System library calls are handled by a dedicated lightweight runtime, and GPU commands are translated directly to Vulkan with support for AMD FSR 3.1 and FSR 4 temporal upscaling.
|
||||
|
||||
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
|
||||
and several areas of Yharnam were played with it) with sound, gamepad and saving.
|
||||
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
|
||||
Mesa/RADV) has been tested thoroughly.
|
||||
> **No game files or copyrighted assets are included.** You must provide your own decrypted dump of Bloodborne (CUSA03173, version 1.09).
|
||||
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware, or AMD.
|
||||
|
||||
## Highlights
|
||||
**Status: Playable on Windows.** The game boots, loads saves, and runs with audio, gamepad support, and state persistence.
|
||||
|
||||
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
|
||||
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
|
||||
translation: the game code runs at full speed.
|
||||
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
|
||||
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
|
||||
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
|
||||
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
|
||||
computes motion vectors itself: camera motion from depth and the scene matrices, and object
|
||||
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
|
||||
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
|
||||
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan).
|
||||
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
|
||||
RDNA2/3 included (see Requirements).
|
||||
- **FSR 4.1.1 (INT8)**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes
|
||||
are replayed natively on Vulkan; the output is **bit-exact** with the DLL. The assets are
|
||||
built on your machine from your own DLLs (`tools/fsr4cap`).
|
||||
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
|
||||
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
|
||||
4K on an RX 7800 XT instead of ~6 ms.
|
||||
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
|
||||
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
|
||||
thread and helper threads for memory copies. Early on the single GPU thread capped the game
|
||||
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
|
||||
- **In-game menu** (Insert or L3+R3): upscaler, preset, sharpness, output resolution, game
|
||||
effects (chromatic aberration, DoF, motion blur, SSAO, the game's own AA, SSR, model LOD).
|
||||
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
|
||||
---
|
||||
|
||||
## How it differs from shadPS4
|
||||
## Windows Port Changes
|
||||
|
||||
| | shadPS4 | bbport |
|
||||
|---|---|---|
|
||||
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
|
||||
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
|
||||
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
|
||||
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
|
||||
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
|
||||
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
|
||||
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
|
||||
This repository adapts the original Linux codebase specifically for Windows systems:
|
||||
|
||||
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
|
||||
the graphics side.
|
||||
- **Win32 Memory & Synchronization**: Replaced Linux `mmap`, `mprotect`, and POSIX primitives with Windows `VirtualAlloc`, `VirtualProtect`, and native Win32 synchronization events (`src/win32_compat.c`, `src/win32_memory.c`, `src/runtime_host.c`).
|
||||
- **Graphical Launcher**: Includes `launch_gui.bat` (powered by `launcher.py`), a desktop control panel to configure target frame rates (30, 60, 90, uncap), rendering resolution, FSR upscaling, and all 61 toggleable community patches from `Bloodborne.xml`.
|
||||
- **Command Line Launcher**: `run.bat` provides direct scripted startup, automatic patch compilation, and mod staging on Windows.
|
||||
- **Vulkan Frame Pacing**: Integrated presenter queue pacing (`BB_FRAMES_AHEAD=2`) to bound GPU run-ahead, stabilizing frametimes and smoothing 0.1% and 1.0% stutter lows.
|
||||
- **Texture Barrier Fix**: Fixed dynamic texture streaming race conditions in `tile_manager.cpp`, ensuring item icons in the HUD and loading screens render correctly without static noise.
|
||||
- **AT9 Audio Playback**: Bundles LibAtrac9 compilation to decode AT9 game audio natively on Windows.
|
||||
- **Windows Build System**: Standalone `build.bat` script that compiles the complete project with MinGW-w64 (GCC), CMake, and Ninja.
|
||||
|
||||
---
|
||||
|
||||
## Technical Highlights
|
||||
|
||||
- **Native Execution**: The game eboot is converted offline into a flat memory image; PS4 libc and libSceFios2 functions are linked directly into the binary. No CPU emulation or instruction translation occurs at run time.
|
||||
- **High Framerate Support**: Community simulation patches allow running at 60 FPS, 90 FPS, or uncapped with accurate physics and movement speeds.
|
||||
- **Synthetic Motion Vectors & Temporal Upscaling**: Because Bloodborne lacks an internal velocity buffer, the port computes camera motion from depth buffers and object motion from previous frame vertex positions. The scene can render at lower internal resolutions (e.g., 720p or 1080p) while FSR 3.1 / 4 upscales to your display resolution, leaving UI elements crisp at native resolution.
|
||||
- **Multi-threaded Draw Pipeline**: PS4 command buffers are decoded on one thread while draw calls are bound and recorded on another, preventing CPU bottlenecks.
|
||||
- **In-Game Overlay**: Press `Insert` or `L3 + R3` on your controller to open the real-time configuration menu for resolution, sharpness, and graphics toggles.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
|
||||
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
|
||||
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
|
||||
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
|
||||
before the first frame and are disabled in the in-game menu.
|
||||
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
|
||||
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
|
||||
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
|
||||
- **Operating System**: Windows 10 or Windows 11 (64-bit).
|
||||
- **Graphics Card**: A Vulkan 1.3 capable GPU with current vendor drivers:
|
||||
- NVIDIA GeForce GTX 10-series or newer.
|
||||
- AMD Radeon RX 400-series or newer.
|
||||
- Intel Arc A-series or newer.
|
||||
- **Processor**: x86-64 processor with AVX2 support.
|
||||
- **Python**: Python 3.10 or newer (needed for the patch compiler and launcher).
|
||||
- **Game Files**: Decrypted `CUSA03173` directory containing `eboot.bin` (version 1.09).
|
||||
|
||||
## Build and run
|
||||
### Build Dependencies (Only if compiling from source)
|
||||
- MinGW-w64 GCC (via [w64devkit](https://github.com/skeeto/w64devkit) or MSYS2 MinGW64).
|
||||
- CMake 3.25+.
|
||||
- Ninja build tool.
|
||||
- SDL3 development libraries.
|
||||
- Vulkan SDK or Vulkan-Headers.
|
||||
|
||||
```bash
|
||||
git clone --recursive <this repository> bbport && cd bbport
|
||||
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
|
||||
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
|
||||
---
|
||||
|
||||
## Building and Running
|
||||
|
||||
### 1. Build from Source
|
||||
|
||||
Clone the repository recursively:
|
||||
```cmd
|
||||
git clone --recursive https://github.com/Mrsuss60/bloodborne_pc_windows_port.git
|
||||
cd bloodborne_pc_windows_port
|
||||
```
|
||||
|
||||
or the launcher (pick the game folder, settings, *Start*):
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
|
||||
Ensure MinGW, CMake, and Ninja are accessible in your environment or PATH, then run:
|
||||
```cmd
|
||||
build.bat
|
||||
```
|
||||
|
||||
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
|
||||
shader cache go to `user/` (the launcher lets you choose another folder); settings to
|
||||
`bbport.ini`. A gamepad is used through SDL3; there is a keyboard fallback.
|
||||
This compiles LibAtrac9, the Vulkan video core (`out/gpu/bbgpu.dll`), and the main loader executable (`out/bbport.exe`).
|
||||
|
||||
**Resolution and preset changes:** for outputs other than 1080p (720p on the Steam Deck,
|
||||
1440p, 4K) the whole game renders at the preset's resolution, set by a patch at start — the
|
||||
fastest path. Changing the output or the preset in the in-game menu then needs *Apply and
|
||||
restart the game*. The *Live resolution changes* setting (launcher, in-game menu,
|
||||
`bbport.ini` `live_resolution=0|1|auto`; **off by default**) instead keeps the game at
|
||||
1080p internally and scales its render targets at run time, so 720p/1080p/1440p/4K and the
|
||||
presets switch without a restart. It costs more: the game then believes it renders 1080p
|
||||
and draws more (e.g. ~8× more small lights), and some targets are copied between sizes —
|
||||
use it on strong desktop GPUs only (`auto` turns it on for discrete GPUs with 8+ GB that are
|
||||
not pre-Turing NVIDIA). 1080p output and TAA always use the live path.
|
||||
### 2. Launch the Game
|
||||
|
||||
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
|
||||
switchable live without an FSR model. The saved FSR preset is restored when returning
|
||||
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
|
||||
be slower than disabling AA. TAA also adds work compared with no temporal AA.
|
||||
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
|
||||
temporal accumulation and leaves its history and HUD unchanged.
|
||||
#### Option A: Using the Graphical Launcher (Recommended)
|
||||
Double-click `launch_gui.bat` or run:
|
||||
```cmd
|
||||
launch_gui.bat
|
||||
```
|
||||
- Select your `eboot.bin` file or game folder.
|
||||
- Select your target frame rate (60 FPS recommended for smooth frame pacing).
|
||||
- Select your internal resolution (e.g., 1280x720 with FSR enabled for high performance).
|
||||
- Enable desired patches under the Patches tab (e.g., *Performance Patch*, *Disable Motion Blur*, *Skip Intro*).
|
||||
- Click **Launch Bloodborne**.
|
||||
|
||||
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
|
||||
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
|
||||
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
|
||||
The original game is preserved; later mods override conflicting files.
|
||||
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
|
||||
switched on and off in the launcher. See [mods and patches](docs/MODS.md).
|
||||
|
||||
**Launcher language:** Russian or English (follows the system language by default).
|
||||
|
||||
**Free camera and game debug menu** (v1.09): enable the corresponding switches in the
|
||||
launcher or in-game menu and restart. Free camera uses Lance McDonald's
|
||||
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
|
||||
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
|
||||
and conflicts with *Enemy Control*.
|
||||
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
|
||||
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
|
||||
the unsafe patch. Open it with the left touchpad / Tab; Backspace is the right touchpad.
|
||||
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
|
||||
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
|
||||
|
||||
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
|
||||
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
|
||||
not implement GPU occlusion culling.
|
||||
|
||||
**Upscaler assets** (not included; FSR 3.1 needs none):
|
||||
|
||||
```bash
|
||||
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
|
||||
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs Proton (GE-Proton):
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll> <amd_fidelityfx_loader_dx12.dll>
|
||||
#### Option B: Using the Command Line
|
||||
Run `run.bat` pointing to your game directory:
|
||||
```cmd
|
||||
run.bat "C:\Games\Bloodborne\CUSA03173"
|
||||
```
|
||||
|
||||
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
|
||||
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them
|
||||
(see above) into `~/.local/share/bbport/fsr4_411` (`BB_PACKAGE_FSR411=1` bundles a local
|
||||
`fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
|
||||
game is 16:9; on the 1280×800 screen it gets thin bars).
|
||||
Save files and shader caches are stored in `user/`. Settings are saved to `bbport.ini`.
|
||||
|
||||
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
|
||||
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
|
||||
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
|
||||
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
|
||||
AppImage*), set the launch options to
|
||||
---
|
||||
|
||||
```
|
||||
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
|
||||
```
|
||||
## Controls and In-Game Features
|
||||
|
||||
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
|
||||
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
|
||||
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
|
||||
` --play` after `%command%` to skip the launcher.
|
||||
- **Gamepad**: Controllers are supported out of the box through SDL3.
|
||||
- **In-Game Settings**: Press `Insert` on keyboard or `L3 + R3` on gamepad to toggle the overlay menu.
|
||||
- **Free Camera**: When enabled in the launcher or overlay, hold `Cross` and press `L3` (or hold `Space` and press `Z` on keyboard) to cycle camera modes.
|
||||
- **Debug Menu**: If debug fonts (`DbgFont14h.ccm` and `DbgFont14h.tpf`) are installed into `dvdroot_ps4/font/`, open the menu using `Tab` or the controller touchpad.
|
||||
|
||||
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
|
||||
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
|
||||
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
|
||||
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
|
||||
In that case copy the NVIDIA libraries into an accessible directory and set
|
||||
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
|
||||
Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
|
||||
the package with:
|
||||
---
|
||||
|
||||
```bash
|
||||
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
|
||||
```
|
||||
## Repository Layout
|
||||
|
||||
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
|
||||
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
|
||||
|
||||
MangoHud is bundled in the AppImage; enable its checkbox in the launcher.
|
||||
When running from source, install MangoHud separately. A diagnostic launch with
|
||||
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
|
||||
|
||||
Useful variables: `BB_FRAME_STATS=1` (frame statistics), `BB_GPU_PROFILE=1` (GPU time per
|
||||
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
|
||||
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
|
||||
0 = unbounded), `BB_PRESENT_THREAD=0` (present on the vblank thread, as before),
|
||||
`BB_LIVE_RES=1` (live resolution changes instead of the startup patch for outputs other than 1080p),
|
||||
`BB_PAD_RECORD=file` / `BB_PAD_REPLAY=file` (record a route with F9, replay it in scripted tests),
|
||||
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_PRESENT_DUMP_TRIGGER=file`
|
||||
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames).
|
||||
More in [docs/](docs); recent changes: [docs/CHANGES_2026-10-02.md](docs/CHANGES_2026-10-02.md),
|
||||
[docs/CHANGES_2026-10-03.md](docs/CHANGES_2026-10-03.md).
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Contents |
|
||||
| Path | Description |
|
||||
|---|---|
|
||||
| `src/` | Loader (`probe.c`) and the HLE runtime |
|
||||
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
|
||||
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
|
||||
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
|
||||
| `patches/` | Community patches for Bloodborne |
|
||||
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
|
||||
| `tests/` | Loader, runtime, patch and renderer tests |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
|
||||
| `src/` | Win32 loader (`probe.c`), memory management, and PS4 HLE runtime |
|
||||
| `gpu/` | Vulkan video core, multi-stage command pipeline, and upscaler integration |
|
||||
| `scripts/` | Eboot preparation, patch compiler (`patches.py`), and DLL staging |
|
||||
| `patches/` | Community XML patches for Bloodborne version 1.09 |
|
||||
| `launcher.py` | Tkinter desktop launcher for Windows |
|
||||
| `launch_gui.bat` | One-click shortcut for the graphical launcher |
|
||||
| `build.bat` | Windows build script |
|
||||
| `run.bat` | Direct execution batch script |
|
||||
| `tools/` | Developer utilities and patch extraction scripts |
|
||||
| `docs/` | Architecture notes, upscaler documentation, and change logs |
|
||||
|
||||
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test`.
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
## Credits and Licenses
|
||||
|
||||
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
|
||||
scaling to all hardware threads — most important for the Steam Deck.
|
||||
- Async compute for the upscaler (the frame is GPU-bound at 4K).
|
||||
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
|
||||
memory (a memory-bridge prototype is in `tools/bridge_helper`); DLSS for NVIDIA users;
|
||||
inputs exposed so that OptiScaler-style mapping works.
|
||||
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
|
||||
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
|
||||
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
|
||||
- Steam Deck validation of the AppImage; HDR output.
|
||||
This project is licensed under the **GNU General Public License v2 or later** ([LICENSE](LICENSE)) due to its use of shadPS4 components.
|
||||
|
||||
## Credits and licenses
|
||||
Special thanks to the original creators and open source projects that made this port possible:
|
||||
|
||||
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
|
||||
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
|
||||
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
|
||||
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
|
||||
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
|
||||
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
|
||||
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
|
||||
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
|
||||
community members (`patches/Bloodborne.xml`). AMD's FSR 4 DLLs and model data are not
|
||||
distributed here.
|
||||
- **deadinside28**: Creator of the original Linux port (`bbport`), pioneering the flat memory image architecture, custom Vulkan two-stage pipeline, and synthetic motion vector generation.
|
||||
- **shadPS4 Team**: Base Vulkan video core and shader recompiler.
|
||||
- **FireBurn**: FSR-Vulkan runtime and temporal upscaling backend.
|
||||
- **Thealexbarney**: LibAtrac9 audio decoding library.
|
||||
- **ocornut**: Dear ImGui library for the in-game settings overlay.
|
||||
- **Community Patch Authors**: Kyo, Lance McDonald, illusion, emoose, auser1337, and contributors for the 60 FPS, camera, and engine patches.
|
||||
@@ -0,0 +1,145 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist "out" mkdir out
|
||||
if not exist "out\gpu" mkdir out\gpu
|
||||
|
||||
if defined W64DEVKIT_DIR (
|
||||
set "PATH=%W64DEVKIT_DIR%\bin;%PATH%"
|
||||
)
|
||||
if defined MINGW_DIR (
|
||||
set "PATH=%MINGW_DIR%\bin;%PATH%"
|
||||
) else if exist "C:\msys64\mingw64\bin" (
|
||||
set "PATH=C:\msys64\mingw64\bin;%PATH%"
|
||||
)
|
||||
|
||||
if exist "C:\Program Files\CMake\bin" (
|
||||
set "PATH=C:\Program Files\CMake\bin;%PATH%"
|
||||
)
|
||||
|
||||
if not exist "out\libatrac9.a" (
|
||||
echo Building LibAtrac9
|
||||
if not exist "out\atrac9" mkdir out\atrac9
|
||||
for %%f in (third_party\LibAtrac9\C\src\*.c) do (
|
||||
gcc -std=c99 -O2 -g -w -c "%%f" -o "out\atrac9\%%~nf.o"
|
||||
if errorlevel 1 (
|
||||
echo Failed to compile LibAtrac9 source: %%f
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
ar rcs out\libatrac9.a out\atrac9\*.o
|
||||
if errorlevel 1 (
|
||||
echo Failed to create out\libatrac9.a
|
||||
exit /b 1
|
||||
)
|
||||
echo Built out\libatrac9.a
|
||||
)
|
||||
|
||||
if exist "patches\fsr_vulkan_mingw.patch" if exist "gpu\third_party\fsr-vulkan\.git" (
|
||||
git -C gpu\third_party\fsr-vulkan apply --check "..\..\..\patches\fsr_vulkan_mingw.patch" >nul 2>nul
|
||||
if not errorlevel 1 (
|
||||
echo Applying MinGW compatibility patch to FSR-Vulkan submodule...
|
||||
git -C gpu\third_party\fsr-vulkan apply "..\..\..\patches\fsr_vulkan_mingw.patch"
|
||||
)
|
||||
)
|
||||
|
||||
if not exist "out\gpu\build.ninja" (
|
||||
echo Configuring CMake (Ninja)
|
||||
set "CMAKE_OPTS=-S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo"
|
||||
where ninja.exe >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
if defined W64DEVKIT_DIR if exist "%W64DEVKIT_DIR%\bin\ninja.exe" (
|
||||
set "CMAKE_OPTS=%CMAKE_OPTS% -DCMAKE_MAKE_PROGRAM=%W64DEVKIT_DIR%/bin/ninja.exe"
|
||||
)
|
||||
)
|
||||
set "PREFIX_PATHS="
|
||||
if defined SDL3_DIR set "PREFIX_PATHS=%SDL3_DIR%"
|
||||
if defined VULKAN_SDK (
|
||||
if defined PREFIX_PATHS (
|
||||
set "PREFIX_PATHS=%PREFIX_PATHS%;%VULKAN_SDK%"
|
||||
) else (
|
||||
set "PREFIX_PATHS=%VULKAN_SDK%"
|
||||
)
|
||||
)
|
||||
if exist "C:\msys64\mingw64" (
|
||||
if defined PREFIX_PATHS (
|
||||
set "PREFIX_PATHS=%PREFIX_PATHS%;C:/msys64/mingw64"
|
||||
) else (
|
||||
set "PREFIX_PATHS=C:/msys64/mingw64"
|
||||
)
|
||||
)
|
||||
if defined CMAKE_PREFIX_PATH (
|
||||
if defined PREFIX_PATHS (
|
||||
set "PREFIX_PATHS=%PREFIX_PATHS%;%CMAKE_PREFIX_PATH%"
|
||||
) else (
|
||||
set "PREFIX_PATHS=%CMAKE_PREFIX_PATH%"
|
||||
)
|
||||
)
|
||||
cmake %CMAKE_OPTS% -DCMAKE_PREFIX_PATH="%PREFIX_PATHS%"
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed.
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo Building bbgpu, bbport, and bb-gpu-capabilities
|
||||
cmake --build out/gpu --target bbgpu bbport bb-gpu-capabilities -- -j 1
|
||||
if errorlevel 1 (
|
||||
echo Build failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if exist "out\bbport.exe" (
|
||||
copy /Y "out\bbport.exe" "out\bb-probe.exe" >nul
|
||||
)
|
||||
|
||||
python scripts\stage_dlls.py
|
||||
|
||||
echo Build complete: out\bbport.exe
|
||||
|
||||
if "%~1"=="--test" (
|
||||
echo Running unit tests
|
||||
set "SDL3_INC="
|
||||
if defined SDL3_DIR (
|
||||
set "SDL3_INC=-I%SDL3_DIR%/include"
|
||||
) else if exist "C:\msys64\mingw64\include\SDL3" (
|
||||
set "SDL3_INC=-IC:/msys64/mingw64/include"
|
||||
)
|
||||
gcc -std=c11 -O2 -g -Wall -Wextra -Werror -I. -Isrc %SDL3_INC% tests/test_pad.c out/SDL3.dll -o out/pad-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
out\pad-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
out\test_runtime.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
gcc -std=c11 -O2 -g -Wall -Wextra -Werror -Isrc tests/test_file_mods.c -o out/file-mods-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
out\file-mods-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
out\test_sema.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
out\content-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
cmake --build out/gpu --target motion-history-test motion-shader-test ui-composition-test upscaler-support-test
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
out\gpu\motion-history-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
out\gpu\ui-composition-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
out\gpu\upscaler-support-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
out\gpu\motion-shader-test.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
python -c "import glob, subprocess, sys; results = [(f, subprocess.run([sys.executable, f]).returncode) for f in sorted(glob.glob('tests/test_*.py'))]; [print(f, code) for f, code in results]; sys.exit(0 if all(code == 0 for f, code in results) else 1)"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
echo ALL TESTS PASSED!
|
||||
)
|
||||
+127
-15
@@ -4,6 +4,8 @@ cmake_minimum_required(VERSION 3.24)
|
||||
project(bbgpu LANGUAGES C CXX)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE RelWithDebInfo)
|
||||
@@ -40,19 +42,85 @@ elseif (BB_PGO STREQUAL "use")
|
||||
-fprofile-correction -Wno-missing-profile -Wno-coverage-mismatch)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
if (DEFINED ENV{MINGW_PREFIX})
|
||||
list(APPEND CMAKE_PREFIX_PATH "$ENV{MINGW_PREFIX}")
|
||||
elseif (EXISTS "C:/msys64/mingw64")
|
||||
list(APPEND CMAKE_PREFIX_PATH "C:/msys64/mingw64")
|
||||
endif()
|
||||
if (DEFINED ENV{SDL3_DIR})
|
||||
list(APPEND CMAKE_PREFIX_PATH "$ENV{SDL3_DIR}")
|
||||
endif()
|
||||
if (DEFINED ENV{VULKAN_SDK})
|
||||
list(APPEND CMAKE_PREFIX_PATH "$ENV{VULKAN_SDK}")
|
||||
endif()
|
||||
add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||
if (EXISTS "C:/msys64/mingw64/lib")
|
||||
link_directories("C:/msys64/mingw64/lib")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(Vulkan REQUIRED)
|
||||
find_package(fmt REQUIRED)
|
||||
find_package(Boost REQUIRED)
|
||||
find_package(magic_enum REQUIRED)
|
||||
if (WIN32)
|
||||
if (NOT TARGET magic_enum::magic_enum)
|
||||
add_library(magic_enum::magic_enum INTERFACE IMPORTED)
|
||||
find_path(MAGIC_ENUM_INC NAMES magic_enum/magic_enum.hpp PATHS "$ENV{MINGW_PREFIX}/include" "C:/msys64/mingw64/include")
|
||||
if (MAGIC_ENUM_INC)
|
||||
target_include_directories(magic_enum::magic_enum INTERFACE "${MAGIC_ENUM_INC}")
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
find_package(magic_enum REQUIRED)
|
||||
endif()
|
||||
find_package(tsl-robin-map REQUIRED)
|
||||
find_package(VulkanMemoryAllocator CONFIG REQUIRED)
|
||||
if (NOT TARGET GPUOpen::VulkanMemoryAllocator)
|
||||
add_library(GPUOpen::VulkanMemoryAllocator INTERFACE IMPORTED)
|
||||
find_path(VMA_INC NAMES vk_mem_alloc.h PATHS "$ENV{MINGW_PREFIX}/include" "C:/msys64/mingw64/include")
|
||||
if (VMA_INC)
|
||||
target_include_directories(GPUOpen::VulkanMemoryAllocator INTERFACE "${VMA_INC}")
|
||||
endif()
|
||||
endif()
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
|
||||
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
|
||||
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
|
||||
find_package(miniz REQUIRED)
|
||||
if (WIN32)
|
||||
find_package(SDL3 QUIET CONFIG)
|
||||
if (TARGET SDL3::SDL3)
|
||||
add_library(PkgConfig::SDL3 INTERFACE IMPORTED)
|
||||
target_link_libraries(PkgConfig::SDL3 INTERFACE SDL3::SDL3)
|
||||
else()
|
||||
find_path(SDL3_INC NAMES SDL3/SDL.h PATHS "$ENV{SDL3_DIR}/include" "C:/msys64/mingw64/include")
|
||||
find_library(SDL3_LIB NAMES SDL3 libSDL3.dll.a PATHS "$ENV{SDL3_DIR}/lib" "C:/msys64/mingw64/lib")
|
||||
if (SDL3_INC AND SDL3_LIB)
|
||||
add_library(PkgConfig::SDL3 INTERFACE IMPORTED)
|
||||
target_include_directories(PkgConfig::SDL3 INTERFACE "${SDL3_INC}")
|
||||
target_link_libraries(PkgConfig::SDL3 INTERFACE "${SDL3_LIB}")
|
||||
else()
|
||||
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
|
||||
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
|
||||
endif()
|
||||
if (WIN32)
|
||||
if (NOT TARGET miniz::miniz)
|
||||
add_library(miniz::miniz INTERFACE IMPORTED)
|
||||
find_path(MINIZ_INC NAMES miniz.h PATHS "$ENV{MINGW_PREFIX}/include" "C:/msys64/mingw64/include")
|
||||
if (MINIZ_INC)
|
||||
target_include_directories(miniz::miniz INTERFACE "${MINIZ_INC}")
|
||||
endif()
|
||||
find_library(MINIZ_LIB NAMES miniz libminiz.a PATHS "$ENV{MINGW_PREFIX}/lib" "C:/msys64/mingw64/lib")
|
||||
if (MINIZ_LIB)
|
||||
target_link_libraries(miniz::miniz INTERFACE "${MINIZ_LIB}")
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
find_package(miniz REQUIRED)
|
||||
endif()
|
||||
find_package(Zydis REQUIRED)
|
||||
find_program(GLSLANG_COMPILER NAMES glslang glslangValidator REQUIRED)
|
||||
find_program(GLSLANG_COMPILER NAMES glslang glslangValidator PATHS "C:/msys64/mingw64/bin" REQUIRED)
|
||||
|
||||
set(SIRIT_USE_SYSTEM_SPIRV_HEADERS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(third_party/sirit EXCLUDE_FROM_ALL)
|
||||
@@ -116,11 +184,19 @@ set(COMMON
|
||||
file(GLOB AVPLAYER CONFIGURE_DEPENDS shadps4/core/libraries/avplayer/*.cpp)
|
||||
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libavformat libavcodec libavutil libswscale libswresample)
|
||||
file(GLOB SHIM_SOURCES CONFIGURE_DEPENDS shim/*.cpp shim/*/*.cpp)
|
||||
add_library(bbgpu SHARED ${VIDEO_CORE} ${RECOMPILER} ${COMMON} ${SHIM_SOURCES}
|
||||
shadps4/core/libraries/gnmdriver/gnmdriver.cpp
|
||||
shadps4/core/libraries/videoout/driver.cpp shadps4/core/libraries/videoout/video_out.cpp
|
||||
shadps4/core/libraries/kernel/equeue.cpp ${AVPLAYER}
|
||||
shadps4/core/libraries/videodec/video_utils.cpp)
|
||||
if (WIN32)
|
||||
add_library(bbgpu STATIC ${VIDEO_CORE} ${RECOMPILER} ${COMMON} ${SHIM_SOURCES}
|
||||
shadps4/core/libraries/gnmdriver/gnmdriver.cpp
|
||||
shadps4/core/libraries/videoout/driver.cpp shadps4/core/libraries/videoout/video_out.cpp
|
||||
shadps4/core/libraries/kernel/equeue.cpp ${AVPLAYER}
|
||||
shadps4/core/libraries/videodec/video_utils.cpp)
|
||||
else()
|
||||
add_library(bbgpu SHARED ${VIDEO_CORE} ${RECOMPILER} ${COMMON} ${SHIM_SOURCES}
|
||||
shadps4/core/libraries/gnmdriver/gnmdriver.cpp
|
||||
shadps4/core/libraries/videoout/driver.cpp shadps4/core/libraries/videoout/video_out.cpp
|
||||
shadps4/core/libraries/kernel/equeue.cpp ${AVPLAYER}
|
||||
shadps4/core/libraries/videodec/video_utils.cpp)
|
||||
endif()
|
||||
add_dependencies(bbgpu host_shaders)
|
||||
# In-game settings menu: Dear ImGui (third_party/imgui, MIT) with its Vulkan backend, and an
|
||||
# embedded Cyrillic font (third_party/fonts).
|
||||
@@ -134,12 +210,22 @@ set_source_files_properties(shim/bbport_overlay.cpp PROPERTIES
|
||||
OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf)
|
||||
target_include_directories(bbgpu PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
|
||||
target_compile_definitions(bbgpu PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
|
||||
if (WIN32)
|
||||
target_compile_definitions(bbgpu PRIVATE VK_USE_PLATFORM_WIN32_KHR)
|
||||
endif()
|
||||
target_compile_options(bbgpu PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
|
||||
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
|
||||
target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
|
||||
if (NOT WIN32)
|
||||
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
|
||||
target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
|
||||
endif()
|
||||
target_link_libraries(bbgpu PRIVATE ffx-vulkan::portable ffx-vulkan::fsr4-v07-vulkan)
|
||||
target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
|
||||
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::X11 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
|
||||
if (WIN32)
|
||||
target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
|
||||
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit ws2_32)
|
||||
else()
|
||||
target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
|
||||
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::X11 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
|
||||
endif()
|
||||
|
||||
# Explicit test targets; neither launches the game nor needs a Vulkan device.
|
||||
add_executable(motion-history-test EXCLUDE_FROM_ALL ../tests/test_motion_history.cpp)
|
||||
@@ -184,3 +270,29 @@ add_executable(fsr4-bench EXCLUDE_FROM_ALL tools/fsr4_bench.cpp
|
||||
shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp)
|
||||
target_include_directories(fsr4-bench PRIVATE shadps4/video_core/renderer_vulkan/fsr411)
|
||||
target_link_libraries(fsr4-bench PRIVATE ffx-vulkan::fsr4-v07-vulkan ffx-vulkan::fsr4-v07-assets Vulkan::Vulkan)
|
||||
|
||||
file(GLOB RUNTIME_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../src/runtime*.c")
|
||||
if (WIN32)
|
||||
list(APPEND RUNTIME_SOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src/win32_compat.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src/win32_memory.c"
|
||||
)
|
||||
endif()
|
||||
add_executable(bbport
|
||||
../src/probe.c
|
||||
${RUNTIME_SOURCES}
|
||||
../src/vulkan_smoke.c
|
||||
)
|
||||
target_include_directories(bbport PRIVATE .. ../src)
|
||||
target_link_libraries(bbport PRIVATE bbgpu "${CMAKE_CURRENT_SOURCE_DIR}/../out/libatrac9.a" Vulkan::Vulkan)
|
||||
if (WIN32)
|
||||
target_link_libraries(bbport PRIVATE ws2_32 dbghelp ntdll winmm)
|
||||
target_link_options(bbport PRIVATE "-Wl,--disable-dynamicbase" "-Wl,--disable-high-entropy-va")
|
||||
endif()
|
||||
set_target_properties(bbport PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../out")
|
||||
|
||||
add_executable(bb-gpu-capabilities ../tools/gpu_capabilities.c)
|
||||
target_include_directories(bb-gpu-capabilities PRIVATE .. ../src)
|
||||
target_link_libraries(bb-gpu-capabilities PRIVATE Vulkan::Vulkan)
|
||||
set_target_properties(bb-gpu-capabilities PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../out")
|
||||
|
||||
@@ -32,6 +32,15 @@ int bbgpu_text_input_poll(char *out_utf8, uint64_t size);
|
||||
int bbgpu_overlay_captures_input(void);
|
||||
/* Number of symbols registered by the vendored libraries (diagnostics). */
|
||||
unsigned bbgpu_symbol_count(void);
|
||||
/* Flips and submissions query for heartbeat / diagnostics. */
|
||||
uint64_t bbgpu_get_flip_count(void);
|
||||
uint64_t bbgpu_get_submit_count(void);
|
||||
/* Dumps host GPU / presenter thread states and counters to a log file. */
|
||||
void bbgpu_dump_host_threads_hang(void *file_handle);
|
||||
/* Computes average FPS and p95/p99 frame times in milliseconds over recent flips. */
|
||||
void bbgpu_get_frametime_percentiles(double *avg_fps, double *p95_ms, double *p99_ms);
|
||||
/* Dumps GPU breadcrumbs state to log/crash files. */
|
||||
void bbgpu_dump_breadcrumbs(void *file_handle);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -266,8 +266,7 @@ uintptr_t IOFile::GetFileMapping() {
|
||||
HANDLE mapping = nullptr;
|
||||
|
||||
if (file_access_mode == FileAccessMode::ReadWrite) {
|
||||
mapping = CreateFileMapping2(hfile, NULL, FILE_MAP_WRITE, PAGE_READWRITE, SEC_COMMIT, 0,
|
||||
NULL, NULL, 0);
|
||||
mapping = CreateFileMappingW(hfile, NULL, PAGE_READWRITE, 0, 0, NULL);
|
||||
} else {
|
||||
mapping = hfile;
|
||||
}
|
||||
|
||||
@@ -509,11 +509,13 @@ typedef struct _TEB { /* win32/win64 */
|
||||
static_assert(offsetof(TEB, DeallocationStack) ==
|
||||
0x1478); /* The only member we care about at the moment */
|
||||
|
||||
#if !defined(QUEUE_USER_APC_FLAGS_NONE) && (!defined(NTDDI_WIN10_MN) || NTDDI_VERSION < NTDDI_WIN10_MN)
|
||||
typedef enum _QUEUE_USER_APC_FLAGS {
|
||||
QueueUserApcFlagsNone,
|
||||
QueueUserApcFlagsSpecialUserApc,
|
||||
QueueUserApcFlagsMaxValue
|
||||
} QUEUE_USER_APC_FLAGS;
|
||||
#endif
|
||||
|
||||
typedef union _USER_APC_OPTION {
|
||||
ULONG_PTR UserApcFlags;
|
||||
|
||||
@@ -25,7 +25,11 @@ static_assert(sizeof(u128) == 16, "u128 must be 128 bits wide");
|
||||
using VAddr = uintptr_t;
|
||||
using PAddr = uintptr_t;
|
||||
|
||||
#ifdef _WIN32
|
||||
#define PS4_SYSV_ABI __attribute__((sysv_abi, nothrow))
|
||||
#else
|
||||
#define PS4_SYSV_ABI __attribute__((sysv_abi))
|
||||
#endif
|
||||
|
||||
// UDLs for memory size values
|
||||
constexpr unsigned long long operator""_KB(unsigned long long x) {
|
||||
|
||||
@@ -313,9 +313,20 @@ bool AvPlayerSource::Stop() {
|
||||
m_up_data_streamer->Reset();
|
||||
}
|
||||
|
||||
m_video_decoder_thread.Stop();
|
||||
m_audio_decoder_thread.Stop();
|
||||
m_demuxer_thread.Stop();
|
||||
m_video_decoder_thread.RequestStop();
|
||||
m_audio_decoder_thread.RequestStop();
|
||||
m_demuxer_thread.RequestStop();
|
||||
|
||||
m_video_packets_cv.Notify();
|
||||
m_audio_packets_cv.Notify();
|
||||
m_video_buffers_cv.Notify();
|
||||
m_audio_buffers_cv.Notify();
|
||||
m_video_frames_cv.Notify();
|
||||
m_audio_frames_cv.Notify();
|
||||
|
||||
m_video_decoder_thread.Join();
|
||||
m_audio_decoder_thread.Join();
|
||||
m_demuxer_thread.Join();
|
||||
|
||||
m_current_audio_frame.reset();
|
||||
m_current_video_frame.reset();
|
||||
|
||||
@@ -21,6 +21,11 @@ namespace Libraries::Kernel {
|
||||
extern boost::asio::io_context io_context;
|
||||
extern void KernelSignalRequest();
|
||||
|
||||
extern "C" {
|
||||
void runtime_thread_set_blocked(const char* what, uint64_t resource);
|
||||
void runtime_thread_clear_blocked(void);
|
||||
}
|
||||
|
||||
static std::unordered_map<s32, EqueueInternal*> kqueues;
|
||||
static constexpr auto HrTimerSpinlockThresholdNs = 1200000u;
|
||||
|
||||
@@ -225,7 +230,7 @@ bool EqueueInternal::TriggerEvent(u64 ident, s16 filter, void* trigger_data) {
|
||||
}
|
||||
}
|
||||
}
|
||||
m_cond.notify_one();
|
||||
m_cond.notify_all();
|
||||
return has_found;
|
||||
}
|
||||
|
||||
@@ -479,7 +484,9 @@ int PS4_SYSV_ABI sceKernelWaitEqueue(OrbisKernelEqueue eq, OrbisKernelEvent* ev,
|
||||
return ORBIS_KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
runtime_thread_set_blocked("equeue", static_cast<uint64_t>(eq));
|
||||
*out = equeue->WaitForEvents(ev, num, timo);
|
||||
runtime_thread_clear_blocked();
|
||||
|
||||
if (*out == 0) {
|
||||
return ORBIS_KERNEL_ERROR_ETIMEDOUT;
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
#include "common/assert.h"
|
||||
#include "bbport_toggles.h"
|
||||
#include "video_core/renderer_vulkan/vk_frame_capture.h"
|
||||
@@ -287,14 +289,58 @@ int VideoOutDriver::ChangeBufferAttribute(VideoOutPort* port, s32 attributeIndex
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr size_t FT_HISTORY_CAP = 1024;
|
||||
static std::atomic<size_t> s_ft_count{0};
|
||||
static double s_ft_history[FT_HISTORY_CAP];
|
||||
static std::chrono::steady_clock::time_point s_last_flip_pt;
|
||||
|
||||
void RecordFlipFrameTime() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (s_last_flip_pt.time_since_epoch().count() != 0) {
|
||||
double ms = std::chrono::duration<double, std::milli>(now - s_last_flip_pt).count();
|
||||
size_t idx = s_ft_count.fetch_add(1, std::memory_order_relaxed);
|
||||
s_ft_history[idx % FT_HISTORY_CAP] = ms;
|
||||
}
|
||||
s_last_flip_pt = now;
|
||||
}
|
||||
|
||||
void GetFrametimeStats(double* avg_fps, double* p95_ms, double* p99_ms) {
|
||||
size_t count = s_ft_count.load(std::memory_order_relaxed);
|
||||
if (count == 0) {
|
||||
if (p95_ms) *p95_ms = 0.0;
|
||||
if (p99_ms) *p99_ms = 0.0;
|
||||
return;
|
||||
}
|
||||
size_t n = std::min(count, FT_HISTORY_CAP);
|
||||
std::vector<double> samples;
|
||||
samples.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
samples.push_back(s_ft_history[i]);
|
||||
}
|
||||
std::sort(samples.begin(), samples.end());
|
||||
if (p95_ms) {
|
||||
size_t idx95 = (n * 95) / 100;
|
||||
if (idx95 >= n) idx95 = n - 1;
|
||||
*p95_ms = samples[idx95];
|
||||
}
|
||||
if (p99_ms) {
|
||||
size_t idx99 = (n * 99) / 100;
|
||||
if (idx99 >= n) idx99 = n - 1;
|
||||
*p99_ms = samples[idx99];
|
||||
}
|
||||
}
|
||||
|
||||
void VideoOutDriver::Flip(const Request& req) {
|
||||
// Update HDR status before presenting, then present the frame (bbport: on the swap thread).
|
||||
RunPresenter([this, frame = req.frame, hdr = req.port->is_hdr] {
|
||||
presenter->SetHDR(hdr);
|
||||
presenter->Present(frame);
|
||||
});
|
||||
|
||||
Vulkan::FrameCapture::OnFlip(req.index >= 0 ? req.port->buffer_slots[req.index].address_left
|
||||
: 0);
|
||||
BbStats::flips.fetch_add(1, std::memory_order_relaxed);
|
||||
RecordFlipFrameTime();
|
||||
|
||||
// bbport: BB_FRAME_STATS=1 prints flip rate and frame time spread every 5 seconds.
|
||||
static const bool frame_stats = EmulatorSettingsImpl::Flag("BB_FRAME_STATS", false);
|
||||
@@ -331,9 +377,11 @@ void VideoOutDriver::Flip(const Request& req) {
|
||||
last_twf = twf;
|
||||
const u64 copy_ns = BbStats::t_copy.load(), copy_bytes = BbStats::copy_bytes.load();
|
||||
u64 proc_flt = 0;
|
||||
#ifndef _WIN32
|
||||
if (rusage usage{}; getrusage(RUSAGE_SELF, &usage) == 0) {
|
||||
proc_flt = usage.ru_minflt;
|
||||
}
|
||||
#endif
|
||||
const u64 t_now[6] = {BbStats::t_resident.load(), BbStats::t_protect.load(),
|
||||
BbStats::t_image_create.load(), BbStats::t_refresh.load(),
|
||||
BbStats::t_staging.load(), BbStats::t_host_wait.load()};
|
||||
@@ -343,11 +391,13 @@ void VideoOutDriver::Flip(const Request& req) {
|
||||
user_us = BbStats::gpu_user_us.load(), invol = BbStats::gpu_invol_switches.load(),
|
||||
vol = BbStats::gpu_vol_switches.load();
|
||||
u64 gpu_ns = 0;
|
||||
#ifndef _WIN32
|
||||
if (const int clock = BbStats::gpu_thread_clock.load(); clock != -1) {
|
||||
timespec ts{};
|
||||
clock_gettime(static_cast<clockid_t>(clock), &ts);
|
||||
gpu_ns = u64(ts.tv_sec) * 1000000000ull + u64(ts.tv_nsec);
|
||||
}
|
||||
#endif
|
||||
const u64 images = BbStats::images_registered.load();
|
||||
const u64 image_bytes = BbStats::image_upload_bytes.load();
|
||||
const u64 buffer_bytes = BbStats::buffer_upload_bytes.load();
|
||||
|
||||
@@ -25,14 +25,21 @@
|
||||
|
||||
#ifdef ARCH_X86_64
|
||||
|
||||
#include <mutex>
|
||||
|
||||
using namespace Xbyak::util;
|
||||
|
||||
static Xbyak::CodeGenerator g_srt_codegen(32_MB);
|
||||
static const u8* g_srt_codegen_start = nullptr;
|
||||
|
||||
namespace {
|
||||
void EnsureSrtFaultHandler();
|
||||
}
|
||||
|
||||
namespace Shader {
|
||||
|
||||
PFN_SrtWalker RegisterWalkerCode(const u8* ptr, size_t size) {
|
||||
EnsureSrtFaultHandler();
|
||||
const auto func_addr = (PFN_SrtWalker)g_srt_codegen.getCurr();
|
||||
g_srt_codegen.db(ptr, size);
|
||||
g_srt_codegen.ready();
|
||||
@@ -77,6 +84,16 @@ static bool SrtWalkerSignalHandler(void* context, void* fault_address) {
|
||||
return false; // Not in SRT code range
|
||||
}
|
||||
|
||||
// bbport: draw-preparation workers run the same walker concurrently; a second thread that
|
||||
// faulted on the instruction before the first one patched it finds the patch and retries.
|
||||
static std::mutex patch_mutex;
|
||||
std::scoped_lock patch_lock{patch_mutex};
|
||||
const u8* bytes = reinterpret_cast<const u8*>(code);
|
||||
if ((bytes[0] == 0x48 && bytes[1] == 0x31 && bytes[2] == 0xFF) ||
|
||||
(bytes[0] == 0x45 && bytes[1] == 0x31 && bytes[2] == 0xD2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Patch instruction to zero register
|
||||
ZydisDecodedInstruction instruction;
|
||||
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
|
||||
@@ -120,6 +137,15 @@ static bool SrtWalkerSignalHandler(void* context, void* fault_address) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EnsureSrtFaultHandler() {
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [] {
|
||||
g_srt_codegen_start = g_srt_codegen.getCurr();
|
||||
constexpr u32 priority = 1;
|
||||
Core::Signals::Instance()->RegisterAccessViolationHandler(SrtWalkerSignalHandler, priority);
|
||||
});
|
||||
}
|
||||
|
||||
using namespace Shader;
|
||||
|
||||
struct PassInfo {
|
||||
@@ -641,13 +667,7 @@ static void GenerateSrtProgram(Info& info, PassInfo& pass_info) {
|
||||
}
|
||||
|
||||
// Register the signal handler for SRT walker, if not already registered
|
||||
if (g_srt_codegen_start == nullptr) {
|
||||
g_srt_codegen_start = c.getCurr();
|
||||
auto* signals = Core::Signals::Instance();
|
||||
// Call after the memory invalidation handler
|
||||
constexpr u32 priority = 1;
|
||||
signals->RegisterAccessViolationHandler(SrtWalkerSignalHandler, priority);
|
||||
}
|
||||
EnsureSrtFaultHandler();
|
||||
|
||||
info.srt_info.walker_func = c.getCurr<PFN_SrtWalker>();
|
||||
pass_info.dst_off_dw = NUM_USER_DATA_REGS;
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <pthread.h>
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
#include <time.h>
|
||||
#include "bbport_copy.h"
|
||||
#include "bbport_toggles.h"
|
||||
@@ -102,12 +109,16 @@ void Liverpool::ProcessCommands() {
|
||||
|
||||
void Liverpool::Process(std::stop_token stoken) {
|
||||
Common::SetCurrentThreadName("shadPS4:GpuCommandProcessor");
|
||||
#ifndef _WIN32
|
||||
if (clockid_t clock; pthread_getcpuclockid(pthread_self(), &clock) == 0) {
|
||||
BbStats::gpu_thread_clock.store(static_cast<int>(clock));
|
||||
}
|
||||
#endif
|
||||
gpu_id = std::this_thread::get_id();
|
||||
#ifdef __linux__
|
||||
gpu_tid = gettid();
|
||||
#elif defined(_WIN32)
|
||||
gpu_tid = GetCurrentThreadId();
|
||||
#endif
|
||||
|
||||
while (!stoken.stop_requested()) {
|
||||
@@ -235,13 +246,29 @@ bool PipelinedOpcode(PM4ItOpcode opcode) {
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
// bbport: a label written straight into guest memory (no backing view): one store. A memcpy of a
|
||||
// variable 4 or 8 bytes is two overlapping stores in glibc; a thread preempted between them
|
||||
// wrote the label again after the guest had freed its block (runtime_memory_write_backing).
|
||||
void StoreLabel(void* address, u64 value, u32 num_bytes) {
|
||||
if (num_bytes == 8 && (reinterpret_cast<uintptr_t>(address) & 7) == 0) {
|
||||
__atomic_store_n(static_cast<u64*>(address), value, __ATOMIC_RELEASE);
|
||||
} else if (num_bytes == 4 && (reinterpret_cast<uintptr_t>(address) & 3) == 0) {
|
||||
__atomic_store_n(static_cast<u32*>(address), u32(value), __ATOMIC_RELEASE);
|
||||
} else {
|
||||
auto* dst = static_cast<u8*>(address);
|
||||
for (u32 i = 0; i < num_bytes; ++i) {
|
||||
__atomic_store_n(dst + i, u8(value >> (8 * i)), __ATOMIC_RELEASE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalEop(const PM4CmdEventWriteEop& eop) {
|
||||
eop.SignalFence(
|
||||
[](void* address, u64 data, u32 num_bytes) {
|
||||
auto* memory = Core::Memory::Instance();
|
||||
BbWriteLog::Note(reinterpret_cast<u64>(address), &data, num_bytes, BbWriteLog::Fence);
|
||||
if (!memory->TryWriteBacking(address, &data, num_bytes)) {
|
||||
memcpy(address, &data, num_bytes);
|
||||
StoreLabel(address, data, num_bytes);
|
||||
}
|
||||
},
|
||||
[] { Platform::IrqC::Instance()->Signal(Platform::InterruptId::GfxEop); });
|
||||
@@ -336,7 +363,7 @@ void RunEventWriteEos(Vulkan::Rasterizer& rasterizer, const u8* data) {
|
||||
auto* memory = Core::Memory::Instance();
|
||||
BbWriteLog::Note(reinterpret_cast<u64>(address), &value, num_bytes, BbWriteLog::Fence);
|
||||
if (!memory->TryWriteBacking(address, &value, num_bytes)) {
|
||||
memcpy(address, &value, num_bytes);
|
||||
StoreLabel(address, value, num_bytes);
|
||||
}
|
||||
});
|
||||
if (event_eos.command == PM4CmdEventWriteEos::Command::GdsStore) {
|
||||
@@ -1325,7 +1352,7 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span<const u32> dcb, std::span<c
|
||||
draw_prep->EndSubmission();
|
||||
}
|
||||
if (seq != NoSeq && BbStats::enabled) {
|
||||
BbStats::submissions.fetch_add(1, std::memory_order_relaxed);
|
||||
#ifndef _WIN32
|
||||
if (rusage usage{}; getrusage(RUSAGE_THREAD, &usage) == 0) {
|
||||
BbStats::gpu_user_us.store(u64(usage.ru_utime.tv_sec) * 1000000 + usage.ru_utime.tv_usec,
|
||||
std::memory_order_relaxed);
|
||||
@@ -1335,6 +1362,18 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span<const u32> dcb, std::span<c
|
||||
BbStats::gpu_vol_switches.store(usage.ru_nvcsw, std::memory_order_relaxed);
|
||||
BbStats::gpu_minor_faults.store(usage.ru_minflt, std::memory_order_relaxed);
|
||||
}
|
||||
#else
|
||||
FILETIME creation, exit, kernel, user;
|
||||
if (GetThreadTimes(GetCurrentThread(), &creation, &exit, &kernel, &user)) {
|
||||
ULARGE_INTEGER k, u;
|
||||
k.LowPart = kernel.dwLowDateTime;
|
||||
k.HighPart = kernel.dwHighDateTime;
|
||||
u.LowPart = user.dwLowDateTime;
|
||||
u.HighPart = user.dwHighDateTime;
|
||||
BbStats::gpu_user_us.store(u.QuadPart / 10, std::memory_order_relaxed);
|
||||
BbStats::gpu_sys_us.store(k.QuadPart / 10, std::memory_order_relaxed);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
FIBER_EXIT;
|
||||
@@ -1684,6 +1723,7 @@ void Liverpool::SubmitGfx(std::span<const u32> dcb, std::span<const u32> ccb) {
|
||||
|
||||
std::scoped_lock lk{submit_mutex};
|
||||
++num_submits;
|
||||
BbStats::submissions.fetch_add(1, std::memory_order_relaxed);
|
||||
work_retired = false;
|
||||
submit_cv.notify_one();
|
||||
}
|
||||
@@ -1702,6 +1742,7 @@ void Liverpool::SubmitAsc(u32 gnm_vqid, std::span<const u32> acb) {
|
||||
std::scoped_lock lk{submit_mutex};
|
||||
num_mapped_queues = std::max(num_mapped_queues, gnm_vqid + 1);
|
||||
++num_submits;
|
||||
BbStats::submissions.fetch_add(1, std::memory_order_relaxed);
|
||||
work_retired = false;
|
||||
submit_cv.notify_one();
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ public:
|
||||
return gpu_id;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
#if defined(__linux__) || defined(_WIN32)
|
||||
u32 GetGpuCommandProcessorThreadId() {
|
||||
return gpu_tid;
|
||||
}
|
||||
@@ -201,11 +201,15 @@ private:
|
||||
return {};
|
||||
}
|
||||
void unhandled_exception() {
|
||||
#if __cpp_exceptions
|
||||
try {
|
||||
std::rethrow_exception(std::current_exception());
|
||||
} catch (const std::exception& e) {
|
||||
UNREACHABLE_MSG("Unhandled exception: {}", e.what());
|
||||
}
|
||||
#else
|
||||
std::terminate();
|
||||
#endif
|
||||
}
|
||||
void return_void() {}
|
||||
struct empty {};
|
||||
@@ -277,7 +281,7 @@ private:
|
||||
std::condition_variable_any submit_cv;
|
||||
std::queue<Common::UniqueFunction<void>> command_queue{};
|
||||
std::thread::id gpu_id;
|
||||
#ifdef __linux__
|
||||
#if defined(__linux__) || defined(_WIN32)
|
||||
u32 gpu_tid;
|
||||
#endif
|
||||
s32 curr_qid{-1};
|
||||
|
||||
@@ -955,19 +955,19 @@ struct PM4CmdReleaseMem {
|
||||
void SignalFence(auto&& signal_irq, auto&& gds_to_mem) const {
|
||||
switch (data_sel.Value()) {
|
||||
case DataSelect::Data32Low: {
|
||||
*Address<u32*>() = DataDWord();
|
||||
__atomic_store_n(Address<u32*>(), DataDWord(), __ATOMIC_RELEASE);
|
||||
break;
|
||||
}
|
||||
case DataSelect::Data64: {
|
||||
*Address<u64*>() = DataQWord();
|
||||
__atomic_store_n(Address<u64*>(), DataQWord(), __ATOMIC_RELEASE);
|
||||
break;
|
||||
}
|
||||
case DataSelect::GpuClock64: {
|
||||
*Address<u64*>() = GetGpuClock64();
|
||||
__atomic_store_n(Address<u64*>(), GetGpuClock64(), __ATOMIC_RELEASE);
|
||||
break;
|
||||
}
|
||||
case DataSelect::PerfCounter: {
|
||||
*Address<u64*>() = GetGpuPerfCounter();
|
||||
__atomic_store_n(Address<u64*>(), GetGpuPerfCounter(), __ATOMIC_RELEASE);
|
||||
break;
|
||||
}
|
||||
case DataSelect::GdsMemStore: {
|
||||
|
||||
@@ -38,7 +38,11 @@ void main() {
|
||||
// Depth was rasterised through a shifted viewport. Reconstruct its unjittered
|
||||
// position, and keep jitter out of FSR's motion vectors (as for the object vectors).
|
||||
const vec2 scene_uv = uv - jitter / size;
|
||||
const vec2 ndc = vec2(scene_uv.x * 2.0 - 1.0, 1.0 - scene_uv.y * 2.0);
|
||||
// bbport: view +y maps down the screen (pixel rows). Checked against the GPU's own object
|
||||
// vectors on static meshes (2026-10-06, 13 frames running up the Cathedral Ward stairs):
|
||||
// this convention matches them within 0.05 px, y up was 1.3-3.7 px off. The error is in
|
||||
// pitch and vertical movement, so it showed on stairs (FSR 4 streaks, FSR 3 blur).
|
||||
const vec2 ndc = vec2(scene_uv.x * 2.0 - 1.0, scene_uv.y * 2.0 - 1.0);
|
||||
|
||||
// Clip z = zs * z + zo, w = z; depth = zs + zo / z (zo < 0, depth < zs in front).
|
||||
const bool sky = depth >= 1.0;
|
||||
@@ -51,7 +55,7 @@ void main() {
|
||||
vec3(dot(reproject[0], view), dot(reproject[1], view), dot(reproject[2], view));
|
||||
const vec2 prev_ndc =
|
||||
vec2(prev_view.x * prev_proj.x, prev_view.y * prev_proj.y) / max(prev_view.z, 1e-4);
|
||||
const vec2 prev_uv = vec2(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);
|
||||
const vec2 prev_uv = prev_ndc * 0.5 + 0.5;
|
||||
const vec2 motion = (prev_uv - scene_uv) * size;
|
||||
|
||||
#ifdef DEBUG_OVERLAY
|
||||
|
||||
@@ -23,19 +23,22 @@ public:
|
||||
explicit BindHelper(bool enabled) {
|
||||
if (enabled) {
|
||||
thread = std::jthread([this](std::stop_token stop) { Run(stop); });
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
~BindHelper() {
|
||||
if (thread.joinable()) {
|
||||
if (running) {
|
||||
thread.request_stop();
|
||||
posted.fetch_add(1, std::memory_order_seq_cst);
|
||||
posted.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
/// Asked per draw: a flag, since std::jthread::joinable() is a system call with libc++ on
|
||||
/// Windows (bbport).
|
||||
[[nodiscard]] bool Available() const noexcept {
|
||||
return thread.joinable();
|
||||
return running;
|
||||
}
|
||||
|
||||
/// True on the helper thread (its callees must not join it).
|
||||
@@ -117,6 +120,7 @@ private:
|
||||
alignas(64) std::atomic<u64> done{0};
|
||||
alignas(64) std::atomic<bool> sleeping{false};
|
||||
std::jthread thread;
|
||||
bool running = false;
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,401 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "video_core/renderer_vulkan/vk_breadcrumbs.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
|
||||
extern "C" int runtime_memory_vma_info(uintptr_t address, int* prot, int* type, uintptr_t* end);
|
||||
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
namespace Vulkan::Breadcrumbs {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 MaxStreams = 16;
|
||||
constexpr u32 StreamStride = 64; // bytes of the marker buffer per stream (a cache line)
|
||||
constexpr u64 RingSize = 1u << 15;
|
||||
constexpr u32 ArgSlots = 4096; // indirect dispatch arguments, 16 bytes each
|
||||
constexpr u64 ArgsOffset = MaxStreams * StreamStride; // where they start in the buffer
|
||||
|
||||
struct Entry {
|
||||
u64 id;
|
||||
Crumb crumb;
|
||||
};
|
||||
|
||||
struct Stream {
|
||||
const char* name = nullptr;
|
||||
std::atomic<u64> next{1};
|
||||
std::unique_ptr<Entry[]> ring;
|
||||
};
|
||||
|
||||
std::array<Stream, MaxStreams> streams;
|
||||
std::atomic<u32> num_streams{0};
|
||||
bool enabled = false;
|
||||
std::once_flag init_once;
|
||||
vk::Buffer marker_buffer{};
|
||||
VmaAllocation marker_allocation{};
|
||||
volatile u32* markers = nullptr;
|
||||
volatile u32* args = nullptr;
|
||||
std::atomic<u32> next_args_slot{0};
|
||||
/// What each argument slot was last used for.
|
||||
struct ArgsNote {
|
||||
u64 id;
|
||||
u32 stream;
|
||||
u64 hash;
|
||||
};
|
||||
std::array<ArgsNote, ArgSlots> args_notes{};
|
||||
|
||||
void Init(const Instance& instance) {
|
||||
const char* env = std::getenv("BB_BREADCRUMBS");
|
||||
if (env && env[0] == '0') {
|
||||
return;
|
||||
}
|
||||
if (!instance.IsBufferMarkerSupported()) {
|
||||
std::printf("GPU breadcrumbs: VK_AMD_buffer_marker unavailable, GPU hangs will not be "
|
||||
"located\n");
|
||||
return;
|
||||
}
|
||||
const VkBufferCreateInfo buffer_ci = {
|
||||
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
||||
.size = ArgsOffset + ArgSlots * 16,
|
||||
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
};
|
||||
const VmaAllocationCreateInfo alloc_ci = {
|
||||
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
||||
.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
};
|
||||
VkBuffer buffer{};
|
||||
VmaAllocationInfo info{};
|
||||
if (vmaCreateBuffer(instance.GetAllocator(), &buffer_ci, &alloc_ci, &buffer,
|
||||
&marker_allocation, &info) != VK_SUCCESS ||
|
||||
!info.pMappedData) {
|
||||
std::printf("GPU breadcrumbs: no host-visible buffer, GPU hangs will not be located\n");
|
||||
return;
|
||||
}
|
||||
marker_buffer = vk::Buffer{buffer};
|
||||
markers = static_cast<volatile u32*>(info.pMappedData);
|
||||
for (u32 i = 0; i < (ArgsOffset + ArgSlots * 16) / sizeof(u32); ++i) {
|
||||
markers[i] = 0;
|
||||
}
|
||||
args = markers + ArgsOffset / sizeof(u32);
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
/// Guest memory that may be gone by now: read only where the runtime has it mapped (its direct
|
||||
/// memory is a GPU driver mapping, which process_vm_readv cannot read).
|
||||
size_t ReadGuest(u64 address, void* out, size_t size) {
|
||||
int prot = 0, type = 0;
|
||||
uintptr_t end = 0;
|
||||
if (!address || !runtime_memory_vma_info(address, &prot, &type, &end) || end <= address) {
|
||||
return 0;
|
||||
}
|
||||
size = std::min<size_t>(size, end - address);
|
||||
std::memcpy(out, reinterpret_cast<const void*>(address), size);
|
||||
return size;
|
||||
}
|
||||
|
||||
const char* KindName(Kind kind) {
|
||||
switch (kind) {
|
||||
case Kind::Pass:
|
||||
return "pass";
|
||||
case Kind::Draw:
|
||||
return "draw";
|
||||
case Kind::DrawIndexed:
|
||||
return "indexed draw";
|
||||
case Kind::DrawIndirect:
|
||||
return "indirect draw";
|
||||
case Kind::Dispatch:
|
||||
return "dispatch";
|
||||
case Kind::DispatchIndirect:
|
||||
return "indirect dispatch";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
void PrintEntry(const Stream& stream, u64 id, const char* label) {
|
||||
const Entry& entry = stream.ring[id & (RingSize - 1)];
|
||||
if (entry.id != id) {
|
||||
std::printf(" #%llu %s: no longer kept\n", (unsigned long long)id, label);
|
||||
return;
|
||||
}
|
||||
const Crumb& c = entry.crumb;
|
||||
std::printf(" #%llu %s: %s", (unsigned long long)id, label, KindName(c.kind));
|
||||
switch (c.kind) {
|
||||
case Kind::Pass:
|
||||
std::printf(" %s", c.name ? c.name : "?");
|
||||
break;
|
||||
case Kind::Draw:
|
||||
case Kind::DrawIndexed:
|
||||
std::printf(" vs %016llx ps %016llx, %u %s x %u instances", (unsigned long long)c.hash[0],
|
||||
(unsigned long long)c.hash[1], c.count[0],
|
||||
c.kind == Kind::Draw ? "vertices" : "indices", c.count[1]);
|
||||
break;
|
||||
case Kind::DrawIndirect: {
|
||||
std::printf(" vs %016llx ps %016llx, up to %u draws, stride %u, arguments at %#llx",
|
||||
(unsigned long long)c.hash[0], (unsigned long long)c.hash[1], c.count[0],
|
||||
c.count[1], (unsigned long long)c.address);
|
||||
u32 now[5]{};
|
||||
if (ReadGuest(c.address, now, sizeof(now)) == sizeof(now)) {
|
||||
std::printf(" (guest memory now %u %u %u %u %u)", now[0], now[1], now[2], now[3], now[4]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Kind::Dispatch:
|
||||
std::printf(" cs %016llx, %ux%ux%u groups", (unsigned long long)c.hash[0], c.count[0],
|
||||
c.count[1], c.count[2]);
|
||||
break;
|
||||
case Kind::DispatchIndirect: {
|
||||
std::printf(" cs %016llx, arguments at %#llx", (unsigned long long)c.hash[0],
|
||||
(unsigned long long)c.address);
|
||||
if (c.args_slot < ArgSlots) {
|
||||
const volatile u32* read = args + c.args_slot * 4;
|
||||
std::printf(" (the GPU read %ux%ux%u groups)", read[0], read[1], read[2]);
|
||||
}
|
||||
std::printf(c.count[0] ? ", in place" : ", a copy");
|
||||
u32 now[3]{};
|
||||
if (ReadGuest(c.address, now, sizeof(now)) == sizeof(now)) {
|
||||
std::printf(" (guest memory now %ux%ux%u)", now[0], now[1], now[2]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (u32 i = 0; i < 2; ++i) {
|
||||
if (c.program[i]) {
|
||||
std::printf(", code %#llx", (unsigned long long)c.program[i]);
|
||||
}
|
||||
}
|
||||
std::printf("\n");
|
||||
}
|
||||
|
||||
/// The guest code of the shaders of the stuck command, for disassembly (up to 64 KiB each:
|
||||
/// the program and what follows, its OrbShdr header among it).
|
||||
void DumpPrograms(const Stream& stream, u64 id) {
|
||||
const Entry& entry = stream.ring[id & (RingSize - 1)];
|
||||
if (entry.id != id) {
|
||||
return;
|
||||
}
|
||||
const char* dir = access("out", W_OK) == 0 ? "out/" : "";
|
||||
for (u32 i = 0; i < 2; ++i) {
|
||||
const u64 program = entry.crumb.program[i];
|
||||
if (!program) {
|
||||
continue;
|
||||
}
|
||||
std::array<u8, 64 * 1024> code{};
|
||||
size_t size = 0;
|
||||
// Page by page: the read stops at the first page that is not mapped.
|
||||
while (size < code.size()) {
|
||||
const size_t chunk = std::min<size_t>(4096 - ((program + size) & 4095), code.size() - size);
|
||||
const size_t read = ReadGuest(program + size, code.data() + size, chunk);
|
||||
size += read;
|
||||
if (read != chunk) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
char path[256];
|
||||
std::snprintf(path, sizeof(path), "%shang_shader_%016llx.bin", dir,
|
||||
(unsigned long long)entry.crumb.hash[i]);
|
||||
if (FILE* file = std::fopen(path, "wb")) {
|
||||
std::fwrite(code.data(), 1, size, file);
|
||||
std::fclose(file);
|
||||
std::printf(" shader %016llx code (%zu bytes from %#llx) saved to %s\n",
|
||||
(unsigned long long)entry.crumb.hash[i], size,
|
||||
(unsigned long long)program, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
u32 NewStream(const Instance& instance, const char* name) {
|
||||
std::call_once(init_once, [&] { Init(instance); });
|
||||
const u32 index = num_streams.fetch_add(1, std::memory_order_relaxed);
|
||||
if (index >= MaxStreams) {
|
||||
return MaxStreams;
|
||||
}
|
||||
streams[index].name = name;
|
||||
streams[index].ring = std::make_unique<Entry[]>(RingSize);
|
||||
return index;
|
||||
}
|
||||
|
||||
bool Enabled() noexcept {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
u32 Note(u32 stream_index, const Crumb& crumb) {
|
||||
if (stream_index >= MaxStreams) {
|
||||
return 0;
|
||||
}
|
||||
Stream& stream = streams[stream_index];
|
||||
const u64 id = stream.next.fetch_add(1, std::memory_order_relaxed);
|
||||
Entry& entry = stream.ring[id & (RingSize - 1)];
|
||||
entry.crumb = crumb;
|
||||
entry.id = id;
|
||||
if (crumb.args_slot < ArgSlots) {
|
||||
args_notes[crumb.args_slot] = {id, stream_index, crumb.hash[0]};
|
||||
}
|
||||
return u32(id);
|
||||
}
|
||||
|
||||
void Mark(vk::CommandBuffer cmdbuf, u32 stream, u32 id, bool after) {
|
||||
if (!enabled || stream >= MaxStreams) {
|
||||
return;
|
||||
}
|
||||
cmdbuf.writeBufferMarkerAMD(vk::PipelineStageFlagBits::eBottomOfPipe, marker_buffer,
|
||||
stream * StreamStride, id * 2 + (after ? 1 : 0));
|
||||
}
|
||||
|
||||
u32 ArgsSlot() {
|
||||
return enabled ? next_args_slot.fetch_add(1, std::memory_order_relaxed) % ArgSlots : ~0u;
|
||||
}
|
||||
|
||||
void CopyArgs(vk::CommandBuffer cmdbuf, vk::Buffer source, u64 offset, u32 slot) {
|
||||
if (!enabled || slot >= ArgSlots) {
|
||||
return;
|
||||
}
|
||||
cmdbuf.copyBuffer(source, marker_buffer,
|
||||
vk::BufferCopy{offset, ArgsOffset + u64(slot) * 16, 3 * sizeof(u32)});
|
||||
}
|
||||
|
||||
namespace {
|
||||
void Report(const char* title, const char* where);
|
||||
}
|
||||
|
||||
void ReportDeviceLost(const char* where) {
|
||||
static std::atomic_flag reported = ATOMIC_FLAG_INIT;
|
||||
if (enabled && !reported.test_and_set()) {
|
||||
Report("at device lost", where);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportStuck(const char* where) {
|
||||
static std::atomic_flag reported = ATOMIC_FLAG_INIT;
|
||||
if (enabled && !reported.test_and_set()) {
|
||||
Report("while the GPU makes no progress", where);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
void Report(const char* title, const char* where) {
|
||||
std::printf("GPU breadcrumbs %s (%s): what each command stream finished\n", title, where);
|
||||
const u32 count = std::min(num_streams.load(), MaxStreams);
|
||||
for (u32 s = 0; s < count; ++s) {
|
||||
const Stream& stream = streams[s];
|
||||
const u64 last = stream.next.load() - 1;
|
||||
const u32 marker = markers[s * StreamStride / sizeof(u32)];
|
||||
std::printf(" stream %u (%s): %llu commands noted", s, stream.name,
|
||||
(unsigned long long)last);
|
||||
if (last == 0) {
|
||||
std::printf("\n");
|
||||
continue;
|
||||
}
|
||||
if (marker == 0) {
|
||||
std::printf(", the GPU finished none\n");
|
||||
continue;
|
||||
}
|
||||
// The marker holds the low 32 bits of 2 * id + after.
|
||||
const u64 newest = last * 2 + 1;
|
||||
const u64 value = newest - u32(u32(newest) - marker);
|
||||
const u64 id = value / 2;
|
||||
if (value & 1) {
|
||||
if (id == last) {
|
||||
std::printf(", the GPU finished all\n");
|
||||
continue;
|
||||
}
|
||||
std::printf(", the GPU finished #%llu but did not start #%llu: stuck in the work "
|
||||
"recorded between them (copies, clears, barriers) or before it\n",
|
||||
(unsigned long long)id, (unsigned long long)(id + 1));
|
||||
PrintEntry(stream, id, "finished");
|
||||
PrintEntry(stream, id + 1, "next");
|
||||
} else {
|
||||
std::printf(", the GPU started #%llu and did not finish it (%llu noted after it)\n",
|
||||
(unsigned long long)id, (unsigned long long)(last - id));
|
||||
if (id > 1) {
|
||||
PrintEntry(stream, id - 1, "finished");
|
||||
}
|
||||
PrintEntry(stream, id, "STUCK");
|
||||
std::fflush(stdout);
|
||||
DumpPrograms(stream, id);
|
||||
}
|
||||
for (u64 next = id + 2; next <= std::min(last, id + 4); ++next) {
|
||||
PrintEntry(stream, next, "later");
|
||||
}
|
||||
}
|
||||
std::fflush(stdout);
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
|
||||
void PrintProgress() {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
std::printf("GPU breadcrumbs:");
|
||||
const u32 count = std::min(num_streams.load(), MaxStreams);
|
||||
std::array<u64, MaxStreams> finished{};
|
||||
for (u32 s = 0; s < count; ++s) {
|
||||
const u64 last = streams[s].next.load(std::memory_order_relaxed) - 1;
|
||||
if (last == 0) {
|
||||
continue;
|
||||
}
|
||||
const u32 marker = markers[s * StreamStride / sizeof(u32)];
|
||||
const u64 newest = last * 2 + 1;
|
||||
const u64 value = marker ? newest - u32(u32(newest) - marker) : 0;
|
||||
// 2n + 1: n finished; 2n: n started, n - 1 finished.
|
||||
finished[s] = value ? value / 2 - (value & 1 ? 0 : 1) : 0;
|
||||
std::printf(" stream %u (%s) %llu noted, %llu unfinished;", s, streams[s].name,
|
||||
(unsigned long long)last, (unsigned long long)(last - finished[s]));
|
||||
}
|
||||
// The largest indirect dispatches among the last ArgSlots the GPU finished, one per shader:
|
||||
// counts that only grow from frame to frame end in a dispatch that never finishes.
|
||||
struct Largest {
|
||||
u64 hash;
|
||||
u64 groups;
|
||||
u32 dims[3];
|
||||
};
|
||||
std::array<Largest, 3> largest{};
|
||||
for (u32 slot = 0; slot < ArgSlots; ++slot) {
|
||||
const ArgsNote note = args_notes[slot];
|
||||
if (note.id == 0 || note.stream >= count || note.id > finished[note.stream]) {
|
||||
continue;
|
||||
}
|
||||
const u32 x = args[slot * 4], y = args[slot * 4 + 1], z = args[slot * 4 + 2];
|
||||
const u64 groups = u64(x) * y * z;
|
||||
auto it = std::find_if(largest.begin(), largest.end(),
|
||||
[&](const Largest& l) { return l.hash == note.hash; });
|
||||
if (it == largest.end()) {
|
||||
it = std::min_element(largest.begin(), largest.end(),
|
||||
[](const Largest& a, const Largest& b) { return a.groups < b.groups; });
|
||||
if (it->groups >= groups) {
|
||||
continue;
|
||||
}
|
||||
*it = {note.hash, 0, {}};
|
||||
}
|
||||
if (groups >= it->groups) {
|
||||
*it = {note.hash, groups, {x, y, z}};
|
||||
}
|
||||
}
|
||||
std::sort(largest.begin(), largest.end(),
|
||||
[](const Largest& a, const Largest& b) { return a.groups > b.groups; });
|
||||
std::printf(" largest indirect dispatches:");
|
||||
for (const auto& l : largest) {
|
||||
if (l.groups != 0) {
|
||||
std::printf(" cs %016llx %ux%ux%u;", (unsigned long long)l.hash, l.dims[0], l.dims[1],
|
||||
l.dims[2]);
|
||||
}
|
||||
}
|
||||
std::printf("\n");
|
||||
}
|
||||
|
||||
} // namespace Vulkan::Breadcrumbs
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// bbport: GPU breadcrumbs, to name the command a GPU hang (ring timeout, then device lost) is
|
||||
// stuck in. Each scheduler is a stream: its draws, dispatches and our own passes are numbered in
|
||||
// recording order, and the GPU writes 2n before command n and 2n+1 after it into a host-visible
|
||||
// buffer (VK_AMD_buffer_marker at the bottom of the pipe: written once all earlier work of the
|
||||
// queue has finished). On device lost the last value says which command never finished, and the
|
||||
// notes kept here say what it was. BB_BREADCRUMBS=0 turns them off.
|
||||
#pragma once
|
||||
|
||||
#include "common/types.h"
|
||||
#include "video_core/renderer_vulkan/vk_common.h"
|
||||
|
||||
namespace Vulkan {
|
||||
class Instance;
|
||||
}
|
||||
|
||||
namespace Vulkan::Breadcrumbs {
|
||||
|
||||
enum class Kind : u8 { Pass, Draw, DrawIndexed, DrawIndirect, Dispatch, DispatchIndirect };
|
||||
|
||||
struct Crumb {
|
||||
Kind kind = Kind::Pass;
|
||||
const char* name = nullptr; ///< our passes: a string literal
|
||||
u64 hash[2]{}; ///< guest shaders: vertex and pixel, or compute
|
||||
u64 program[2]{}; ///< their code in guest memory
|
||||
u64 address = 0; ///< indirect arguments
|
||||
u32 count[3]{}; ///< indices or vertices and instances, or groups
|
||||
u32 args_slot = ~0u; ///< indirect dispatches: where the GPU copies its arguments
|
||||
};
|
||||
|
||||
/// A stream for a new scheduler (`name` a string literal); the first call creates the buffer.
|
||||
u32 NewStream(const Instance& instance, const char* name);
|
||||
[[nodiscard]] bool Enabled() noexcept;
|
||||
/// Recording side, in command order: numbers the next command of `stream`.
|
||||
u32 Note(u32 stream, const Crumb& crumb);
|
||||
/// Recorded right before (`after` false) and right after the command numbered `id`.
|
||||
void Mark(vk::CommandBuffer cmdbuf, u32 stream, u32 id, bool after);
|
||||
/// Indirect dispatches: a slot of a host-visible buffer for the arguments the GPU reads (the
|
||||
/// report and the statistics show them), ~0u when off. CopyArgs is recorded before the dispatch.
|
||||
u32 ArgsSlot();
|
||||
void CopyArgs(vk::CommandBuffer cmdbuf, vk::Buffer args, u64 offset, u32 slot);
|
||||
/// Device lost: prints, per stream, the command the GPU did not finish and what it was.
|
||||
void ReportDeviceLost(const char* where);
|
||||
/// The same report, once, while the GPU has not finished submitted work for a long time (no
|
||||
/// device lost yet: a reset may never come, or the waiting thread would not see it).
|
||||
void ReportStuck(const char* where);
|
||||
/// Frame statistics: per stream, the commands noted and how many the GPU has yet to finish.
|
||||
void PrintProgress();
|
||||
|
||||
/// Breadcrumbs around what is recorded on `cmdbuf` right here (not deferred) in the scope.
|
||||
class Scope {
|
||||
public:
|
||||
Scope(vk::CommandBuffer cmdbuf_, u32 stream_, const char* name)
|
||||
: cmdbuf{cmdbuf_}, stream{stream_} {
|
||||
if (Enabled()) {
|
||||
id = Note(stream, {.name = name});
|
||||
Mark(cmdbuf, stream, id, false);
|
||||
}
|
||||
}
|
||||
~Scope() {
|
||||
if (id) {
|
||||
Mark(cmdbuf, stream, id, true);
|
||||
}
|
||||
}
|
||||
Scope(const Scope&) = delete;
|
||||
Scope& operator=(const Scope&) = delete;
|
||||
|
||||
private:
|
||||
vk::CommandBuffer cmdbuf;
|
||||
u32 stream;
|
||||
u32 id = 0;
|
||||
};
|
||||
|
||||
} // namespace Vulkan::Breadcrumbs
|
||||
@@ -159,7 +159,11 @@ private:
|
||||
void Run(std::stop_token stop) {
|
||||
Common::SetCurrentThreadName("bb:DrawRec");
|
||||
on_stage_b = true;
|
||||
#ifdef _WIN32
|
||||
stage_b_tid.store(static_cast<u32>(GetCurrentThreadId()), std::memory_order_release);
|
||||
#else
|
||||
stage_b_tid.store(static_cast<u32>(gettid()), std::memory_order_release);
|
||||
#endif
|
||||
u64 at = 0;
|
||||
while (true) {
|
||||
// Packets follow each other within microseconds while a frame is decoded: spin,
|
||||
|
||||
@@ -274,6 +274,7 @@ bool Instance::CreateDevice() {
|
||||
// Optional
|
||||
maintenance_5 = add_extension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME);
|
||||
maintenance_8 = add_extension(VK_KHR_MAINTENANCE_8_EXTENSION_NAME);
|
||||
buffer_marker = add_extension(VK_AMD_BUFFER_MARKER_EXTENSION_NAME);
|
||||
attachment_feedback_loop = add_extension(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME);
|
||||
if (attachment_feedback_loop) {
|
||||
attachment_feedback_loop =
|
||||
@@ -666,10 +667,19 @@ void Instance::CreateAllocator() {
|
||||
.vkGetDeviceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetDeviceProcAddr,
|
||||
};
|
||||
|
||||
// bbport: 64 MiB blocks (VMA default 256 MiB). A block goes back to the driver only once
|
||||
// empty: images freed here and there (the texture collector) left 256 MiB blocks mostly
|
||||
// empty and held. BB_VMA_BLOCK_MB=N.
|
||||
static const VkDeviceSize block_size = [] {
|
||||
const char* env = std::getenv("BB_VMA_BLOCK_MB");
|
||||
return VkDeviceSize(env ? std::max(1ul, std::strtoul(env, nullptr, 10)) : 64ul) << 20;
|
||||
}();
|
||||
|
||||
const VmaAllocatorCreateInfo allocator_info = {
|
||||
.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT,
|
||||
.physicalDevice = physical_device,
|
||||
.device = *device,
|
||||
.preferredLargeHeapBlockSize = block_size,
|
||||
.pVulkanFunctions = &functions,
|
||||
.instance = *instance,
|
||||
.vulkanApiVersion = TargetVulkanApiVersion,
|
||||
@@ -737,10 +747,15 @@ void Instance::CollectPhysicalMemoryInfo() {
|
||||
// If memory budget is not supported, use the size of the heap as the budget.
|
||||
total_memory_budget += memory_props.memoryHeaps[i].size;
|
||||
}
|
||||
total_local_memory = local_memory;
|
||||
if (!IsIntegrated()) {
|
||||
// We reserve some memory for the system.
|
||||
const u64 system_memory = std::min<u64>(total_memory_budget / 8, 1_GB);
|
||||
total_memory_budget -= system_memory;
|
||||
// Reserve memory for system only if we have plenty of dedicated VRAM (> 4 GB).
|
||||
// On discrete GPUs with <= 4 GB VRAM, the driver's memory budget already accounts
|
||||
// for OS/DWM, so subtracting 1 GB starves the application working set.
|
||||
if (total_memory_budget > 4_GB) {
|
||||
const u64 system_memory = std::min<u64>(total_memory_budget / 8, 1_GB);
|
||||
total_memory_budget -= system_memory;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Leave at least 8 GB for the system on integrated GPUs.
|
||||
@@ -873,6 +888,21 @@ vk::Format Instance::GetSupportedFormat(const vk::Format format,
|
||||
return vk::Format::eR8G8Unorm;
|
||||
}
|
||||
break;
|
||||
case vk::Format::eR32G32B32Sfloat:
|
||||
if (IsFormatSupported(vk::Format::eR32G32B32A32Sfloat, flags)) {
|
||||
return vk::Format::eR32G32B32A32Sfloat;
|
||||
}
|
||||
break;
|
||||
case vk::Format::eR32G32B32Uint:
|
||||
if (IsFormatSupported(vk::Format::eR32G32B32A32Uint, flags)) {
|
||||
return vk::Format::eR32G32B32A32Uint;
|
||||
}
|
||||
break;
|
||||
case vk::Format::eR32G32B32Sint:
|
||||
if (IsFormatSupported(vk::Format::eR32G32B32A32Sint, flags)) {
|
||||
return vk::Format::eR32G32B32A32Sint;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -133,6 +133,11 @@ public:
|
||||
return maintenance_8;
|
||||
}
|
||||
|
||||
/// bbport: VK_AMD_buffer_marker (GPU breadcrumbs).
|
||||
bool IsBufferMarkerSupported() const {
|
||||
return buffer_marker;
|
||||
}
|
||||
|
||||
/// Returns true if VK_EXT_attachment_feedback_loop_layout is supported
|
||||
bool IsAttachmentFeedbackLoopLayoutSupported() const {
|
||||
return attachment_feedback_loop;
|
||||
@@ -485,6 +490,11 @@ public:
|
||||
return total_memory_budget;
|
||||
}
|
||||
|
||||
/// Returns the physical device-local memory size (dedicated VRAM).
|
||||
[[nodiscard]] u64 GetDeviceLocalMemory() const {
|
||||
return total_local_memory;
|
||||
}
|
||||
|
||||
/// Determines if a format is supported for a set of feature flags.
|
||||
[[nodiscard]] bool IsFormatSupported(vk::Format format, vk::FormatFeatureFlags2 flags) const;
|
||||
|
||||
@@ -558,6 +568,7 @@ private:
|
||||
bool workgroup_memory_explicit_layout{};
|
||||
bool maintenance_5{};
|
||||
bool maintenance_8{};
|
||||
bool buffer_marker{};
|
||||
bool attachment_feedback_loop{};
|
||||
bool image_2d_view_of_3d{};
|
||||
bool image_view_min_lod{};
|
||||
@@ -567,6 +578,7 @@ private:
|
||||
bool supports_memory_budget{};
|
||||
bool supports_block_texel_view{};
|
||||
u64 total_memory_budget{};
|
||||
u64 total_local_memory{};
|
||||
std::vector<size_t> valid_heaps;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "bbport_threads.h"
|
||||
|
||||
#include "common/hash.h"
|
||||
#include "common/io_file.h"
|
||||
#include "common/path_util.h"
|
||||
@@ -847,8 +849,8 @@ PipelineCache::Result PipelineCache::GetProgram(PipelineSelection& sel, HwStage
|
||||
// pointers in the registers; ahead of the GPU thread that memory may already be
|
||||
// reused. A fault returns here (runtime_fault_recover) and the draw is left to the GPU
|
||||
// thread. A jump out of the specialization leaks its partial allocations (rare).
|
||||
sigjmp_buf recover;
|
||||
if (sigsetjmp(recover, 0)) {
|
||||
BbRecoverBuf recover;
|
||||
if (BB_RECOVER_SET(recover)) {
|
||||
worker.failed = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -381,8 +381,9 @@ void PipelineCache::WarmUp() {
|
||||
|
||||
LOG_INFO(Render, "Preloaded {} pipelines", num_pipelines);
|
||||
if (num_total_pipelines > num_pipelines) {
|
||||
LOG_WARNING(Render, "{} stale pipelines were found. Consider re-generating the cache",
|
||||
LOG_WARNING(Render, "{} stale pipelines were found: clearing cache to regenerate cleanly",
|
||||
num_total_pipelines - num_pipelines);
|
||||
Storage::DataBase::Instance().Clear();
|
||||
}
|
||||
|
||||
Storage::DataBase::Instance().FinishPreload();
|
||||
|
||||
@@ -161,9 +161,18 @@ Presenter::~Presenter() {
|
||||
draw_scheduler.Finish();
|
||||
present_scheduler.Finish();
|
||||
flip_scheduler.Finish();
|
||||
Check(draw_scheduler.CommandBuffer().reset());
|
||||
Check(present_scheduler.CommandBuffer().reset());
|
||||
Check(flip_scheduler.CommandBuffer().reset());
|
||||
if (rasterizer) {
|
||||
rasterizer->GetPipelineCache().Sync();
|
||||
}
|
||||
if (draw_scheduler.CommandBuffer()) {
|
||||
(void)draw_scheduler.CommandBuffer().reset();
|
||||
}
|
||||
if (present_scheduler.CommandBuffer()) {
|
||||
(void)present_scheduler.CommandBuffer().reset();
|
||||
}
|
||||
if (flip_scheduler.CommandBuffer()) {
|
||||
(void)flip_scheduler.CommandBuffer().reset();
|
||||
}
|
||||
|
||||
const vk::Device device = instance.GetDevice();
|
||||
for (auto& frame : present_frames) {
|
||||
@@ -213,10 +222,23 @@ void Presenter::RecreateFrame(Frame* frame, u32 width, u32 height) {
|
||||
|
||||
VkResult result = vmaCreateImage(instance.GetAllocator(), &unsafe_image_info, &alloc_info,
|
||||
&unsafe_image, &frame->allocation, nullptr);
|
||||
if (result != VK_SUCCESS) {
|
||||
VmaAllocationCreateInfo fallback_alloc = alloc_info;
|
||||
fallback_alloc.flags = 0;
|
||||
result = vmaCreateImage(instance.GetAllocator(), &unsafe_image_info, &fallback_alloc,
|
||||
&unsafe_image, &frame->allocation, nullptr);
|
||||
}
|
||||
if (result != VK_SUCCESS) [[unlikely]] {
|
||||
LOG_CRITICAL(Render_Vulkan, "Failed allocating texture with error {}",
|
||||
LOG_CRITICAL(Render_Vulkan, "Failed allocating frame texture with error {}",
|
||||
vk::to_string(vk::Result{result}));
|
||||
UNREACHABLE();
|
||||
VmaAllocationCreateInfo sys_alloc = alloc_info;
|
||||
sys_alloc.flags = 0;
|
||||
sys_alloc.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
|
||||
result = vmaCreateImage(instance.GetAllocator(), &unsafe_image_info, &sys_alloc,
|
||||
&unsafe_image, &frame->allocation, nullptr);
|
||||
if (result != VK_SUCCESS) {
|
||||
UNREACHABLE();
|
||||
}
|
||||
}
|
||||
frame->image = vk::Image{unsafe_image};
|
||||
SetObjectName(device, frame->image, "Frame image #{}", frame->id);
|
||||
|
||||
@@ -740,8 +740,8 @@ void Rasterizer::RunDrawPacket(void* context, const u8* data, u32 size) {
|
||||
std::memcpy(flat.data(), snapshot.user_data,
|
||||
std::min(snapshot.user_data_size, snapshot.flat_size) * sizeof(u32));
|
||||
// Pointers in the tables may be stale by now: a fault only skips the check.
|
||||
sigjmp_buf recover;
|
||||
if (sigsetjmp(recover, 0)) {
|
||||
BbRecoverBuf recover;
|
||||
if (BB_RECOVER_SET(recover)) {
|
||||
runtime_fault_recover = nullptr;
|
||||
continue;
|
||||
}
|
||||
@@ -1507,6 +1507,7 @@ void Rasterizer::JoinBindHelper(void* context) {
|
||||
}
|
||||
|
||||
bool Rasterizer::BindResources(const Pipeline* pipeline) {
|
||||
pipeline_is_compute = pipeline->IsCompute();
|
||||
if (IsComputeImageCopy(pipeline) || IsComputeMetaClear(pipeline) ||
|
||||
IsComputeImageClear(pipeline)) {
|
||||
return false;
|
||||
@@ -2460,12 +2461,59 @@ void Rasterizer::BindTextures(const Shader::Info& stage, const PreparedStage* pr
|
||||
BindSamplers(stage, prepared, binding, write_index);
|
||||
}
|
||||
|
||||
// bbport: the PS4 build samples most scene textures with little or no anisotropic filtering.
|
||||
// Behind the temporal upscaler the output shows the detail of the output's mip level, and
|
||||
// surfaces seen at a grazing angle (stairs, floors, walls along the view) stay blurred.
|
||||
// Mipmapped linear samplers of graphics draws get BB_ANISO (16; 0 = the game's) times.
|
||||
void Rasterizer::ForceAnisotropy(AmdGpu::Sampler& sharp, bool is_depth) const {
|
||||
static const int forced = [this] {
|
||||
const char* value = std::getenv("BB_ANISO");
|
||||
int target = value ? std::atoi(value) : 16;
|
||||
if (target > 0) {
|
||||
const float max_aniso = instance.MaxSamplerAnisotropy();
|
||||
if (max_aniso > 0.0f && target > static_cast<int>(max_aniso)) {
|
||||
target = static_cast<int>(max_aniso);
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}();
|
||||
if (!is_depth && !pipeline_is_compute) {
|
||||
// The game's filters, once per kind: min filter, mip filter, ratio.
|
||||
static std::atomic<u64> seen{0};
|
||||
const u32 kind = u32(sharp.xy_min_filter.Value()) * 3 * 5 + u32(sharp.mip_filter.Value()) * 5 +
|
||||
u32(sharp.max_aniso.Value());
|
||||
if (kind < 64 && !(seen.fetch_or(1ull << kind) & (1ull << kind))) {
|
||||
std::printf("Sampler: game uses min filter %u, mip filter %u, anisotropy %.0fx\n",
|
||||
u32(sharp.xy_min_filter.Value()), u32(sharp.mip_filter.Value()),
|
||||
sharp.MaxAniso());
|
||||
}
|
||||
}
|
||||
if (forced <= 1 || is_depth || pipeline_is_compute || BbToggle::Disabled(BbToggle::ForcedAniso) ||
|
||||
sharp.mip_filter == AmdGpu::MipFilter::None ||
|
||||
(sharp.xy_min_filter != AmdGpu::Filter::Bilinear &&
|
||||
sharp.xy_min_filter != AmdGpu::Filter::AnisoLinear)) {
|
||||
return;
|
||||
}
|
||||
const auto ratio = forced >= 16 ? AmdGpu::AnisoRatio::Sixteen
|
||||
: forced >= 8 ? AmdGpu::AnisoRatio::Eight
|
||||
: forced >= 4 ? AmdGpu::AnisoRatio::Four : AmdGpu::AnisoRatio::Two;
|
||||
if (AmdGpu::IsAnisoFilter(sharp.xy_min_filter) && sharp.MaxAniso() >= forced) {
|
||||
return;
|
||||
}
|
||||
sharp.xy_min_filter.Assign(AmdGpu::Filter::AnisoLinear);
|
||||
if (sharp.xy_mag_filter == AmdGpu::Filter::Bilinear) {
|
||||
sharp.xy_mag_filter.Assign(AmdGpu::Filter::AnisoLinear);
|
||||
}
|
||||
sharp.max_aniso.Assign(ratio);
|
||||
}
|
||||
|
||||
void Rasterizer::BindSamplers(const Shader::Info& stage, const PreparedStage* prepared,
|
||||
Shader::Backend::Bindings& binding, u32& write_index) {
|
||||
for (u32 sampler_index = 0; sampler_index < stage.samplers.size(); ++sampler_index) {
|
||||
const auto& sampler = stage.samplers[sampler_index];
|
||||
auto ssharp =
|
||||
prepared ? prepared->sampler_sharps[sampler_index] : sampler.GetSharp(stage);
|
||||
ForceAnisotropy(ssharp, sampler.is_depth);
|
||||
const auto vk_sampler = texture_cache.GetSampler(
|
||||
ssharp, Regs().ta_bc_base, sampler.is_depth, sampler.is_depth ? 0.0f : sampler_lod_bias);
|
||||
image_infos.emplace_back(vk_sampler, VK_NULL_HANDLE, vk::ImageLayout::eGeneral);
|
||||
|
||||
@@ -252,6 +252,7 @@ private:
|
||||
Shader::Backend::Bindings& binding, u32& write_index, bool& barrier,
|
||||
bool on_helper = false);
|
||||
bool BindResources(const Pipeline* pipeline);
|
||||
void ForceAnisotropy(AmdGpu::Sampler& sharp, bool is_depth) const;
|
||||
void BindSamplers(const Shader::Info& stage, const PreparedStage* prepared,
|
||||
Shader::Backend::Bindings& binding, u32& write_index);
|
||||
/// bbport: a stage's resolved textures remembered by its prepared T# hashes
|
||||
@@ -490,6 +491,7 @@ private:
|
||||
u32 pending_checks = 0;
|
||||
u64 proxy_samples = 0; ///< texture bindings that read a scene proxy (statistics)
|
||||
float sampler_lod_bias = 0.0f; ///< bbport: extra bias of this draw's samplers
|
||||
bool pipeline_is_compute = false; ///< bbport: the bound pipeline of BindResources
|
||||
bool scene_debug_frame = false; ///< BB_SCENE_DEBUG: this frame's passes are printed
|
||||
bool PendingWriteOverlaps(VAddr address, u64 size);
|
||||
/// Stage B: the ring bindings of the stages of the packet being recorded.
|
||||
|
||||
@@ -44,36 +44,25 @@ static std::pair<u32, u32> SanitizeCopyLayers(const VideoCore::ImageInfo& src_in
|
||||
|
||||
// 3D images can only use 1 layer.
|
||||
if (vk_src_type == vk::ImageType::e3D && src_layers != 1) {
|
||||
LOG_WARNING(Render_Vulkan, "Coercing copy 3D source layers {} to 1.", src_layers);
|
||||
src_layers = 1;
|
||||
}
|
||||
if (vk_dst_type == vk::ImageType::e3D && dst_layers != 1) {
|
||||
LOG_WARNING(Render_Vulkan, "Coercing copy 3D destination layers {} to 1.", dst_layers);
|
||||
dst_layers = 1;
|
||||
}
|
||||
|
||||
// If the image type is equal, layer count must match. Take the minimum of both.
|
||||
if (vk_src_type == vk_dst_type) {
|
||||
if (src_layers != dst_layers) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Coercing copy source layers {} and destination layers {} to minimum.",
|
||||
src_layers, dst_layers);
|
||||
src_layers = dst_layers = std::min(src_layers, dst_layers);
|
||||
}
|
||||
} else {
|
||||
// For 2D <-> 3D copies, 2D layer count must equal 3D depth.
|
||||
if (vk_src_type == vk::ImageType::e2D && vk_dst_type == vk::ImageType::e3D &&
|
||||
src_layers != depth) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Coercing copy 2D source layers {} to 3D destination depth {}", src_layers,
|
||||
depth);
|
||||
src_layers = depth;
|
||||
}
|
||||
if (vk_src_type == vk::ImageType::e3D && vk_dst_type == vk::ImageType::e2D &&
|
||||
dst_layers != depth) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Coercing copy 2D destination layers {} to 3D source depth {}", dst_layers,
|
||||
depth);
|
||||
dst_layers = depth;
|
||||
}
|
||||
}
|
||||
@@ -461,21 +450,18 @@ void Runtime::CopyMip(VideoCore::Image* src, VideoCore::Image* dst, u32 mip, u32
|
||||
ASSERT(mip_block_h == (src->info.size.height >> src_dim));
|
||||
ASSERT(mip_block_p == (src->info.pitch >> src_dim));
|
||||
|
||||
const auto [src_layers, dst_layers] =
|
||||
SanitizeCopyLayers(src->info, dst->info, src->info.size.depth);
|
||||
|
||||
const vk::ImageCopy image_copy{
|
||||
.srcSubresource{
|
||||
.aspectMask = src->aspect_mask,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = src_layers,
|
||||
.layerCount = 1,
|
||||
},
|
||||
.dstSubresource{
|
||||
.aspectMask = src->aspect_mask,
|
||||
.mipLevel = mip,
|
||||
.baseArrayLayer = slice,
|
||||
.layerCount = dst_layers,
|
||||
.layerCount = 1,
|
||||
},
|
||||
.extent = {src->info.size.width, src->info.size.height, src->info.size.depth},
|
||||
};
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <unordered_map>
|
||||
#ifndef _WIN32
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <functional>
|
||||
|
||||
#include "bbport_copy.h"
|
||||
@@ -31,6 +33,7 @@ Scheduler::Scheduler(const Instance& instance, bool threaded_recording)
|
||||
if (threaded_recording && !(env && env[0] == '0')) {
|
||||
record_chunk = AcquireChunk();
|
||||
recorder_thread = std::jthread(std::bind_front(&Scheduler::RecorderThread, this));
|
||||
recorder_running = true;
|
||||
}
|
||||
#if TRACY_GPU_ENABLED
|
||||
profiler_scope = reinterpret_cast<tracy::VkCtxScope*>(std::malloc(sizeof(tracy::VkCtxScope)));
|
||||
@@ -41,8 +44,9 @@ Scheduler::Scheduler(const Instance& instance, bool threaded_recording)
|
||||
}
|
||||
|
||||
Scheduler::~Scheduler() {
|
||||
if (recorder_thread.joinable()) {
|
||||
if (recorder_running) {
|
||||
SyncRecording();
|
||||
recorder_running = false;
|
||||
recorder_thread.request_stop();
|
||||
recorder_cv.notify_all();
|
||||
recorder_thread.join();
|
||||
@@ -104,7 +108,7 @@ void Scheduler::BeginRendering(const RenderState& new_state) {
|
||||
.pStencilAttachment = db.has_stencil ? &stencil_attachment : nullptr,
|
||||
};
|
||||
|
||||
if (!recorder_thread.joinable()) {
|
||||
if (!recorder_running) {
|
||||
current_cmdbuf.beginRendering(rendering_info);
|
||||
return;
|
||||
}
|
||||
@@ -152,6 +156,7 @@ void Scheduler::TraceDirectRecording(void* caller) {
|
||||
}
|
||||
std::ranges::sort(top, std::greater{});
|
||||
for (size_t i = 0; i < std::min<size_t>(top.size(), 8); ++i) {
|
||||
#ifndef _WIN32
|
||||
Dl_info info{};
|
||||
dladdr(top[i].second, &info);
|
||||
std::printf("Recorder sync caller: %llu x %s+0x%lx\n",
|
||||
@@ -159,6 +164,11 @@ void Scheduler::TraceDirectRecording(void* caller) {
|
||||
info.dli_fname ? info.dli_fname : "?",
|
||||
static_cast<unsigned long>(reinterpret_cast<uintptr_t>(top[i].second) -
|
||||
reinterpret_cast<uintptr_t>(info.dli_fbase)));
|
||||
#else
|
||||
std::printf("Recorder sync caller: %llu x %p\n",
|
||||
static_cast<unsigned long long>(top[i].first),
|
||||
top[i].second);
|
||||
#endif
|
||||
}
|
||||
callers.clear();
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ public:
|
||||
/// records directly (Record() included, so callers may mix both) until the next
|
||||
/// KickRecording() or submission.
|
||||
[[gnu::noinline]] vk::CommandBuffer CommandBuffer() {
|
||||
if (recorder_thread.joinable() && !direct_mode) {
|
||||
if (recorder_running && !direct_mode) {
|
||||
SyncRecording();
|
||||
direct_mode = true;
|
||||
direct_recordings.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -730,7 +730,7 @@ public:
|
||||
/// everything it uses (capture by value): it may run later on the recording thread.
|
||||
template <typename Func>
|
||||
void Record(Func&& func) {
|
||||
if (!recorder_thread.joinable() || direct_mode) {
|
||||
if (!recorder_running || direct_mode) {
|
||||
func(current_cmdbuf);
|
||||
return;
|
||||
}
|
||||
@@ -750,7 +750,7 @@ public:
|
||||
|
||||
/// True when Record() defers commands (and RecordData() copies into chunks).
|
||||
[[nodiscard]] bool IsRecordingDeferred() const noexcept {
|
||||
return recorder_thread.joinable() && !direct_mode &&
|
||||
return recorder_running && !direct_mode &&
|
||||
!BbToggle::Disabled(BbToggle::ThreadedRecording);
|
||||
}
|
||||
|
||||
@@ -920,6 +920,7 @@ private:
|
||||
std::shared_ptr<std::atomic<u64>> deferred_signals_done =
|
||||
std::make_shared<std::atomic<u64>>(0);
|
||||
std::jthread recorder_thread;
|
||||
bool recorder_running = false;
|
||||
tracy::VkCtxScope* profiler_scope{};
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
#ifdef WAIT_TIMEOUT
|
||||
#undef WAIT_TIMEOUT
|
||||
#endif
|
||||
constexpr u64 WAIT_TIMEOUT = std::numeric_limits<u64>::max();
|
||||
|
||||
Semaphore::Semaphore(const Instance& instance_) : instance{instance_} {
|
||||
|
||||
@@ -102,8 +102,22 @@ void UniqueImage::Destroy() {
|
||||
void UniqueImage::Create(const vk::ImageCreateInfo& image_ci) {
|
||||
this->image_ci = image_ci;
|
||||
ASSERT(!image);
|
||||
// bbport: images of BB_VMA_DEDICATED_MB (4) or more get memory of their own, given back to
|
||||
// the driver when they are freed. In shared VMA blocks the texture collector's evictions left
|
||||
// holes (~0.9 GB on an RX 7800 XT) that a block only returns once wholly empty.
|
||||
static const vk::DeviceSize dedicated_from = [] {
|
||||
const char* env = std::getenv("BB_VMA_DEDICATED_MB");
|
||||
return vk::DeviceSize(env ? std::strtoull(env, nullptr, 10) : 4) << 20;
|
||||
}();
|
||||
VmaAllocationCreateFlags dedicated = 0;
|
||||
if (device && dedicated_from != 0) {
|
||||
const vk::DeviceImageMemoryRequirements query{.pCreateInfo = &image_ci};
|
||||
if (device.getImageMemoryRequirements(query).memoryRequirements.size >= dedicated_from) {
|
||||
dedicated = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT;
|
||||
}
|
||||
}
|
||||
const VmaAllocationCreateInfo alloc_ci = {
|
||||
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT,
|
||||
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | dedicated,
|
||||
.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
|
||||
.requiredFlags = 0,
|
||||
.preferredFlags = 0,
|
||||
|
||||
@@ -46,19 +46,21 @@ TextureCache::TextureCache(const Vulkan::Instance& instance_, Vulkan::Scheduler&
|
||||
return;
|
||||
}
|
||||
|
||||
const s64 device_local_memory = static_cast<s64>(instance.GetTotalMemoryBudget());
|
||||
const s64 min_spacing_expected = device_local_memory - 1_GB;
|
||||
const s64 min_spacing_critical = device_local_memory - 512_MB;
|
||||
const s64 mem_threshold = std::min<s64>(device_local_memory, TARGET_GC_THRESHOLD);
|
||||
const s64 min_vacancy_expected = (6 * mem_threshold) / 10;
|
||||
const s64 min_vacancy_critical = (2 * mem_threshold) / 10;
|
||||
pressure_gc_memory = static_cast<u64>(
|
||||
std::max<u64>(std::min(device_local_memory - min_vacancy_expected, min_spacing_expected),
|
||||
DEFAULT_PRESSURE_GC_MEMORY));
|
||||
critical_gc_memory = static_cast<u64>(
|
||||
std::max<u64>(std::min(device_local_memory - min_vacancy_critical, min_spacing_critical),
|
||||
DEFAULT_CRITICAL_GC_MEMORY));
|
||||
trigger_gc_memory = static_cast<u64>((device_local_memory - mem_threshold) / 2);
|
||||
u64 budget = instance.GetDeviceMemoryBudgetNow() ? instance.GetDeviceMemoryBudgetNow()
|
||||
: instance.GetTotalMemoryBudget();
|
||||
const u64 local_mem = instance.GetDeviceLocalMemory();
|
||||
const bool is_low_vram = (local_mem > 0 && local_mem <= 4096ULL * 1024 * 1024) ||
|
||||
(budget > 0 && budget <= 4096ULL * 1024 * 1024);
|
||||
const u64 base_mem = is_low_vram && local_mem > 0 ? local_mem : budget;
|
||||
if (base_mem > 0) {
|
||||
trigger_gc_memory = base_mem / 100 * (is_low_vram ? 70 : 70);
|
||||
pressure_gc_memory = base_mem / 100 * (is_low_vram ? 90 : 85);
|
||||
critical_gc_memory = base_mem / 100 * (is_low_vram ? 95 : 95);
|
||||
} else {
|
||||
pressure_gc_memory = DEFAULT_PRESSURE_GC_MEMORY;
|
||||
critical_gc_memory = DEFAULT_CRITICAL_GC_MEMORY;
|
||||
trigger_gc_memory = 0;
|
||||
}
|
||||
}
|
||||
|
||||
TextureCache::~TextureCache() = default;
|
||||
@@ -113,11 +115,14 @@ void TextureCache::DownloadImageMemory(ImageId image_id, bool sync) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline u64 MaybeDirtyHash(const Image& image) {
|
||||
return XXH3_64bits(std::bit_cast<const u8*>(image.info.guest_address), image.info.guest_size);
|
||||
}
|
||||
|
||||
void TextureCache::MarkAsMaybeDirty(ImageId image_id, Image& image) {
|
||||
if (image.hash == 0) {
|
||||
// Initialize hash
|
||||
const u8* addr = std::bit_cast<u8*>(image.info.guest_address);
|
||||
image.hash = XXH3_64bits(addr, image.info.guest_size);
|
||||
image.hash = MaybeDirtyHash(image);
|
||||
}
|
||||
image.flags |= ImageFlagBits::MaybeCpuDirty;
|
||||
UntrackImage(image_id);
|
||||
@@ -771,17 +776,7 @@ void TextureCache::RefreshImage(Image& image) {
|
||||
|
||||
if (True(image.flags & ImageFlagBits::MaybeCpuDirty) &&
|
||||
False(image.flags & ImageFlagBits::CpuDirty)) {
|
||||
// The image size should be less than page size to be considered MaybeCpuDirty
|
||||
// So this calculation should be very uncommon and reasonably fast
|
||||
// For now we'll just check up to 64 first pixels
|
||||
const auto addr = std::bit_cast<u8*>(image.info.guest_address);
|
||||
const u32 w = std::min(image.info.size.width, u32(8));
|
||||
const u32 h = std::min(image.info.size.height, u32(8));
|
||||
|
||||
const u32 s_w = image.info.props.is_block ? Common::DivCeil(w, 4u) : w;
|
||||
const u32 s_h = image.info.props.is_block ? Common::DivCeil(h, 4u) : h;
|
||||
const u32 size = s_w * s_h * (image.info.num_bits / 8);
|
||||
const u64 hash = XXH3_64bits(addr, size);
|
||||
const u64 hash = MaybeDirtyHash(image);
|
||||
if (image.hash == hash) {
|
||||
image.flags &= ~ImageFlagBits::MaybeCpuDirty;
|
||||
return;
|
||||
@@ -804,14 +799,12 @@ void TextureCache::RefreshImage(Image& image) {
|
||||
const auto [mip_size, mip_pitch, mip_height, mip_offset] = image.info.mips_layout[m];
|
||||
|
||||
// Protect GPU modified resources from accidental CPU reuploads.
|
||||
if (is_gpu_modified && !is_gpu_dirty) {
|
||||
const u8* addr = std::bit_cast<u8*>(image.info.guest_address);
|
||||
const u64 hash = XXH3_64bits(addr + mip_offset, mip_size);
|
||||
if (image.mip_hashes[m] == hash) {
|
||||
continue;
|
||||
}
|
||||
image.mip_hashes[m] = hash;
|
||||
const u8* mip_addr = std::bit_cast<u8*>(image.info.guest_address) + mip_offset;
|
||||
const u64 mip_hash = XXH3_64bits(mip_addr, mip_size);
|
||||
if (is_gpu_modified && !is_gpu_dirty && image.mip_hashes[m] == mip_hash) {
|
||||
continue;
|
||||
}
|
||||
image.mip_hashes[m] = mip_hash;
|
||||
|
||||
const u32 extent_width = mip_pitch ? std::min(mip_pitch, width) : width;
|
||||
const u32 extent_height = mip_height ? std::min(mip_height, height) : height;
|
||||
@@ -1025,54 +1018,118 @@ void TextureCache::GarbageCollectImages() {
|
||||
const char* env = std::getenv("BB_GC_BUDGET_MB");
|
||||
return env ? std::strtoull(env, nullptr, 10) << 20 : 0;
|
||||
}();
|
||||
if (instance.IsIntegrated() || forced_budget) {
|
||||
const u64 budget = forced_budget ? forced_budget : instance.GetDeviceMemoryBudgetNow();
|
||||
if (budget != 0) {
|
||||
trigger_gc_memory = budget / 10 * 7;
|
||||
pressure_gc_memory = budget / 100 * 85;
|
||||
critical_gc_memory = budget / 100 * 95;
|
||||
}
|
||||
u64 budget = forced_budget ? forced_budget : instance.GetDeviceMemoryBudgetNow();
|
||||
const u64 local_mem = instance.GetDeviceLocalMemory();
|
||||
if (budget != 0 || local_mem != 0) {
|
||||
const bool is_low_vram = (local_mem > 0 && local_mem <= 4096ULL * 1024 * 1024) ||
|
||||
(budget > 0 && budget <= 4096ULL * 1024 * 1024);
|
||||
const u64 base_mem = !forced_budget && is_low_vram && local_mem > 0 ? local_mem : budget;
|
||||
static const u64 trig_pct = [is_low_vram] {
|
||||
const char* e = std::getenv("BB_GC_TRIGGER");
|
||||
return e ? std::strtoull(e, nullptr, 10) : (is_low_vram ? 70 : 70);
|
||||
}();
|
||||
static const u64 pres_pct = [is_low_vram] {
|
||||
const char* e = std::getenv("BB_GC_PRESSURE");
|
||||
return e ? std::strtoull(e, nullptr, 10) : (is_low_vram ? 90 : 80);
|
||||
}();
|
||||
static const u64 crit_pct = [is_low_vram] {
|
||||
const char* e = std::getenv("BB_GC_CRITICAL");
|
||||
return e ? std::strtoull(e, nullptr, 10) : (is_low_vram ? 95 : 88);
|
||||
}();
|
||||
trigger_gc_memory = base_mem / 100 * trig_pct;
|
||||
pressure_gc_memory = base_mem / 100 * pres_pct;
|
||||
critical_gc_memory = base_mem / 100 * crit_pct;
|
||||
}
|
||||
}
|
||||
BbStats::gc_used_bytes.store(total_used_memory, std::memory_order_relaxed);
|
||||
BbStats::gc_trigger_bytes.store(trigger_gc_memory, std::memory_order_relaxed);
|
||||
// bbport: gc_tick (one per guest submission, hundreds a second) at each of the last 64
|
||||
// seconds, for ages in seconds. The usage compared is all of our VRAM, not only images: on a
|
||||
// discrete GPU it stays over the trigger, where an age of 16 ticks (~3 frames) evicted every
|
||||
// texture briefly off screen and uploaded it again. Below the pressure mark an image goes
|
||||
// once unused for BB_GC_IDLE_SECONDS (20): the textures of an area left behind.
|
||||
const u64 second = u64(std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
if (second != gc_second) {
|
||||
gc_second = second;
|
||||
BbStats::coarse_second.store(u32(second), std::memory_order_relaxed);
|
||||
gc_tick_at_second[second % gc_tick_at_second.size()] = gc_tick;
|
||||
}
|
||||
if (total_used_memory < trigger_gc_memory) {
|
||||
return;
|
||||
}
|
||||
static const u64 idle_seconds = [] {
|
||||
const char* env = std::getenv("BB_GC_IDLE_SECONDS");
|
||||
return std::clamp<u64>(env ? std::strtoull(env, nullptr, 10) : 20, 1, 63);
|
||||
}();
|
||||
// The tick at the start of that second (an older one where no submission came then).
|
||||
const u64 idle_tick =
|
||||
gc_tick_at_second[(second - idle_seconds) % gc_tick_at_second.size()];
|
||||
const u64 pressure_seconds = std::clamp<u64>(idle_seconds / 4, 3ULL, 5ULL);
|
||||
const u64 raw_press_tick =
|
||||
gc_tick_at_second[(second - pressure_seconds) % gc_tick_at_second.size()];
|
||||
const u64 pressure_tick = raw_press_tick ? raw_press_tick : (gc_tick > 300 ? gc_tick - 300 : 0);
|
||||
const u64 raw_crit_tick =
|
||||
gc_tick_at_second[(second - 1) % gc_tick_at_second.size()];
|
||||
const u64 critical_tick = raw_crit_tick ? raw_crit_tick : (gc_tick > 120 ? gc_tick - 120 : 0);
|
||||
|
||||
std::scoped_lock lock{mutex};
|
||||
bool pressured = false;
|
||||
bool aggresive = false;
|
||||
u64 ticks_to_destroy = 0;
|
||||
u64 below_tick = 0;
|
||||
size_t num_deletions = 0;
|
||||
u32 visited = 0;
|
||||
u32 sync_downloads_this_pass = 0;
|
||||
|
||||
const auto configure = [&](bool allow_aggressive) {
|
||||
pressured = total_used_memory >= pressure_gc_memory;
|
||||
aggresive = allow_aggressive && total_used_memory >= critical_gc_memory;
|
||||
ticks_to_destroy = aggresive ? 160 : pressured ? 80 : 16;
|
||||
ticks_to_destroy = std::min(ticks_to_destroy, gc_tick);
|
||||
num_deletions = aggresive ? 40 : pressured ? 20 : 10;
|
||||
if (aggresive) {
|
||||
below_tick = critical_tick;
|
||||
num_deletions = 16;
|
||||
} else if (pressured) {
|
||||
below_tick = pressure_tick;
|
||||
num_deletions = 6;
|
||||
} else {
|
||||
below_tick = idle_tick;
|
||||
num_deletions = 4;
|
||||
}
|
||||
visited = 0;
|
||||
};
|
||||
const auto clean_up = [&](ImageId image_id) {
|
||||
if (num_deletions == 0) {
|
||||
if (num_deletions == 0 || ++visited > 256) {
|
||||
return true;
|
||||
}
|
||||
--num_deletions;
|
||||
auto& image = slot_images[image_id];
|
||||
const bool download = image.SafeToDownload();
|
||||
const bool tiled = image.info.IsTiled();
|
||||
if (tiled && download) {
|
||||
// This is a workaround for now. We can't handle non-linear image downloads.
|
||||
return false;
|
||||
}
|
||||
if (download && !pressured) {
|
||||
// bbport: images that cannot go now (GPU-written: their contents exist only here, and
|
||||
// tiled ones cannot be written back at all) neither use up the deletions nor stay first
|
||||
// in line. The oldest ten being such render targets stopped the collector for good, and
|
||||
// VRAM grew with every area visited up to the pressure mark (~10 GB on 16 GB).
|
||||
if ((tiled && download) || (download && !pressured)) {
|
||||
lru_cache.Touch(image.lru_id, gc_tick);
|
||||
BbStats::gc_kept_images.fetch_add(1, std::memory_order_relaxed);
|
||||
return false;
|
||||
}
|
||||
if (download) {
|
||||
if (sync_downloads_this_pass >= 1) {
|
||||
// Rate-limit synchronous GPU flushes to at most 1 per submission pass
|
||||
// to prevent multiple consecutive pipeline stalls and frame hitches.
|
||||
lru_cache.Touch(image.lru_id, gc_tick);
|
||||
return false;
|
||||
}
|
||||
// bbport: synchronously, while the image still protects its pages. A deferred
|
||||
// write-back landed after FreeImage had unprotected them, over whatever the game
|
||||
// had meanwhile stored there (e.g. its heap after unloading an area).
|
||||
// had mapped there since (0x28ce9b5).
|
||||
DownloadImageMemory(image_id, true);
|
||||
++gc_downloads;
|
||||
++sync_downloads_this_pass;
|
||||
}
|
||||
--num_deletions;
|
||||
++gc_evictions;
|
||||
BbStats::gc_freed_images.fetch_add(1, std::memory_order_relaxed);
|
||||
FreeImage(image_id);
|
||||
if (total_used_memory < critical_gc_memory) {
|
||||
if (aggresive) {
|
||||
@@ -1090,12 +1147,12 @@ void TextureCache::GarbageCollectImages() {
|
||||
|
||||
// Try to remove anything old enough and not high priority.
|
||||
configure(false);
|
||||
lru_cache.ForEachItemBelow(gc_tick - ticks_to_destroy, clean_up);
|
||||
lru_cache.ForEachItemBelow(below_tick, clean_up);
|
||||
|
||||
if (total_used_memory >= critical_gc_memory) {
|
||||
// If we are still over the critical limit, run an aggressive GC
|
||||
configure(true);
|
||||
lru_cache.ForEachItemBelow(gc_tick - ticks_to_destroy, clean_up);
|
||||
lru_cache.ForEachItemBelow(below_tick, clean_up);
|
||||
}
|
||||
// bbport: evictions under memory pressure, at most every 5 s (BB_FRAME_STATS or not).
|
||||
if (pressured || gc_downloads != 0) {
|
||||
|
||||
@@ -401,6 +401,9 @@ private:
|
||||
u64 total_used_memory = 0;
|
||||
u64 gc_evictions = 0, gc_downloads = 0; ///< bbport: pressure report
|
||||
std::chrono::steady_clock::time_point gc_report_time{};
|
||||
/// bbport: gc_tick at each of the last 64 seconds (GarbageCollectImages).
|
||||
std::array<u64, 64> gc_tick_at_second{};
|
||||
u64 gc_second = 0;
|
||||
u64 trigger_gc_memory = 0;
|
||||
u64 pressure_gc_memory = 0;
|
||||
u64 critical_gc_memory = 0;
|
||||
|
||||
@@ -268,6 +268,11 @@ std::pair<const Buffer*, u64> TileManager::DetileImage(const VideoCore::Buffer*
|
||||
// bbport: recorded with copies of the descriptor infos (threaded recording).
|
||||
RecordTilingDispatch(GetTilingPipeline(info, false), tiled_buffer_info, linear_buffer_info,
|
||||
params_buffer_info, dim_x);
|
||||
// bbport: the source may be the arena (BB_PREUPLOAD), which later uploads write: a copy into
|
||||
// it must wait for this read.
|
||||
runtime.AccessBuffer(in_buffer, in_offset, info.guest_size,
|
||||
vk::PipelineStageFlagBits2::eComputeShader,
|
||||
vk::AccessFlagBits2::eShaderRead);
|
||||
|
||||
runtime.AccessBuffer(staging.buffer, staging.offset, info.guest_size,
|
||||
vk::PipelineStageFlagBits2::eComputeShader,
|
||||
|
||||
+62
-1
@@ -2,8 +2,9 @@
|
||||
// bbport: glue between the C loader and the vendored shadPS4 video core.
|
||||
#include "bbport_overlay.h"
|
||||
#include "bbport_settings.h"
|
||||
#include "bbport_copy.h"
|
||||
#ifndef _WIN32
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
#include "bbport_toggles.h"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "common/elf_info.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/rdtsc.h"
|
||||
#include "video_core/renderer_vulkan/vk_breadcrumbs.h"
|
||||
#include "core/libraries/kernel/orbis_error.h"
|
||||
#include "core/libraries/libs.h"
|
||||
#include "core/memory.h"
|
||||
@@ -211,7 +213,11 @@ extern "C" int bbgpu_init(const BbGpuConfig* config) {
|
||||
StartProfileWriter();
|
||||
#endif
|
||||
g_sdk_version = config->sdk_version;
|
||||
#ifdef _WIN32
|
||||
if (config->user_dir) _putenv_s("BB_GPU_USER_DIR", config->user_dir);
|
||||
#else
|
||||
if (config->user_dir) setenv("BB_GPU_USER_DIR", config->user_dir, 0);
|
||||
#endif
|
||||
Core::Emulator::FillElfInfo(*config);
|
||||
const std::string title = config->title ? config->title : "Bloodborne";
|
||||
const s32 width = config->width, height = config->height;
|
||||
@@ -270,6 +276,61 @@ extern "C" unsigned bbgpu_symbol_count(void) {
|
||||
return unsigned(g_symbols.size());
|
||||
}
|
||||
|
||||
extern "C" uint64_t bbgpu_get_flip_count(void) {
|
||||
return BbStats::flips.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" uint64_t bbgpu_get_submit_count(void) {
|
||||
return BbStats::submissions.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
namespace Libraries::VideoOut {
|
||||
void GetFrametimeStats(double* avg_fps, double* p95_ms, double* p99_ms);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_get_frametime_percentiles(double* avg_fps, double* p95_ms, double* p99_ms) {
|
||||
Libraries::VideoOut::GetFrametimeStats(avg_fps, p95_ms, p99_ms);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_dump_host_threads_hang(void* file_handle) {
|
||||
FILE* f = static_cast<FILE*>(file_handle);
|
||||
if (!f) return;
|
||||
std::fprintf(f, "=== HOST GPU / PRESENTER THREADS ===\n");
|
||||
std::fprintf(f, "GPU Stats:\n");
|
||||
std::fprintf(f, " Submissions: %llu, Flips: %llu, Draws: %llu, Dispatches: %llu\n",
|
||||
(unsigned long long)BbStats::submissions.load(std::memory_order_relaxed),
|
||||
(unsigned long long)BbStats::flips.load(std::memory_order_relaxed),
|
||||
(unsigned long long)BbStats::draws.load(std::memory_order_relaxed),
|
||||
(unsigned long long)BbStats::dispatches.load(std::memory_order_relaxed));
|
||||
std::fprintf(f, " GPU Idle Time: %.2f ms\n",
|
||||
(double)BbStats::gpu_idle_ns.load(std::memory_order_relaxed) / 1000000.0);
|
||||
std::fprintf(f, " Scheduler Sync Wait: %.2f ms\n",
|
||||
(double)BbStats::sync_recording_ns.load(std::memory_order_relaxed) / 1000000.0);
|
||||
std::fprintf(f, " Host Copies Wait: %.2f ms\n",
|
||||
(double)BbStats::host_copies_wait_ns.load(std::memory_order_relaxed) / 1000000.0);
|
||||
std::fprintf(f, " GPU Tick Wait: %.2f ms\n",
|
||||
(double)BbStats::tick_wait_ns.load(std::memory_order_relaxed) / 1000000.0);
|
||||
std::fprintf(f, " Host Wait (fences/queue): %.2f ms\n",
|
||||
(double)BbStats::t_host_wait.load(std::memory_order_relaxed) / 1000000.0);
|
||||
std::fprintf(f, " Texture Creation Time: %.2f ms, Staging Time: %.2f ms\n",
|
||||
(double)BbStats::t_image_create.load(std::memory_order_relaxed) / 1000000.0,
|
||||
(double)BbStats::t_staging.load(std::memory_order_relaxed) / 1000000.0);
|
||||
std::fprintf(f, " Page Protection Revoke Calls: %llu (%llu pages)\n",
|
||||
(unsigned long long)BbStats::protect_revoke_calls.load(std::memory_order_relaxed),
|
||||
(unsigned long long)BbStats::protect_revoke_pages.load(std::memory_order_relaxed));
|
||||
std::fprintf(f, "====================================\n\n");
|
||||
std::fflush(f);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_dump_breadcrumbs(void* file_handle) {
|
||||
if (!file_handle) return;
|
||||
FILE* f = static_cast<FILE*>(file_handle);
|
||||
std::fprintf(f, "\n=== GPU BREADCRUMBS STATE ===\n");
|
||||
Vulkan::Breadcrumbs::ReportStuck("diagnostic dump");
|
||||
std::fprintf(f, "=============================\n\n");
|
||||
std::fflush(f);
|
||||
}
|
||||
|
||||
// Kernel service thread: runs boost::asio timers for equeue timer events.
|
||||
namespace Libraries::Kernel {
|
||||
boost::asio::io_context io_context;
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
|
||||
// DejaVu Sans (Cyrillic), embedded (third_party/fonts, Bitstream Vera license).
|
||||
#ifdef _WIN32
|
||||
asm(".section .rdata,\"dr\"\n"
|
||||
".balign 16\n"
|
||||
".global bb_font_ttf\n"
|
||||
"bb_font_ttf:\n"
|
||||
".incbin \"" BB_FONT_PATH "\"\n"
|
||||
".global bb_font_ttf_end\n"
|
||||
"bb_font_ttf_end:\n"
|
||||
".text\n");
|
||||
#else
|
||||
asm(".section .rodata\n"
|
||||
".balign 16\n"
|
||||
".hidden bb_font_ttf\n"
|
||||
@@ -26,6 +36,7 @@ asm(".section .rodata\n"
|
||||
".global bb_font_ttf_end\n"
|
||||
"bb_font_ttf_end:\n"
|
||||
".previous\n");
|
||||
#endif
|
||||
extern "C" const unsigned char bb_font_ttf[];
|
||||
extern "C" const unsigned char bb_font_ttf_end[];
|
||||
|
||||
@@ -403,6 +414,12 @@ void FpsCounter() {
|
||||
} // namespace
|
||||
|
||||
void Init(const Vulkan::Instance& instance, vk::Format format, u32 image_count) {
|
||||
if (const char* env = std::getenv("BB_OVERLAY")) {
|
||||
if (!std::strcmp(env, "0") || !std::strcmp(env, "off") || !std::strcmp(env, "false")) {
|
||||
std::printf("Overlay: disabled (BB_OVERLAY=0)\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::scoped_lock lock{imgui_mutex};
|
||||
if (initialized) {
|
||||
return;
|
||||
|
||||
@@ -45,13 +45,13 @@ inline constexpr int OutputCount = 4;
|
||||
inline constexpr int OutputDefault = 1; ///< 1920x1080, the game's own size
|
||||
|
||||
struct Values {
|
||||
std::atomic<int> upscaler{UpscalerFsr3};
|
||||
std::atomic<int> upscaler{UpscalerOff};
|
||||
std::atomic<int> preset{NativeAA};
|
||||
std::atomic<bool> sharpen{true};
|
||||
std::atomic<float> sharpness{0.3f};
|
||||
std::atomic<bool> jitter{true};
|
||||
std::atomic<bool> jitter{false};
|
||||
std::atomic<bool> reactive{false};
|
||||
std::atomic<bool> object_motion{true};
|
||||
std::atomic<bool> object_motion{false};
|
||||
std::atomic<float> reactive_scale{1.0f};
|
||||
std::atomic<float> reactive_threshold{0.2f};
|
||||
std::atomic<float> reactive_max{0.9f};
|
||||
|
||||
@@ -6,29 +6,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
struct alignas(16) BbRecoverBuf {
|
||||
unsigned char registers[256];
|
||||
};
|
||||
extern "C" __thread BbRecoverBuf* runtime_fault_recover;
|
||||
extern "C" int runtime_setjmp(BbRecoverBuf* buf) __attribute__((returns_twice));
|
||||
#define BB_RECOVER_SET(buf) runtime_setjmp(&(buf))
|
||||
#else
|
||||
#include <sched.h>
|
||||
#include <sys/resource.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <setjmp.h>
|
||||
typedef sigjmp_buf BbRecoverBuf;
|
||||
extern "C" __thread BbRecoverBuf* runtime_fault_recover;
|
||||
#define BB_RECOVER_SET(buf) sigsetjmp(buf, 0)
|
||||
#endif
|
||||
|
||||
namespace BbThreads {
|
||||
|
||||
/// Hardware threads available to the process.
|
||||
inline unsigned Available() {
|
||||
#ifdef _WIN32
|
||||
DWORD_PTR process_mask = 0;
|
||||
DWORD_PTR system_mask = 0;
|
||||
if (GetProcessAffinityMask(GetCurrentProcess(), &process_mask, &system_mask) && process_mask != 0) {
|
||||
unsigned count = 0;
|
||||
while (process_mask) {
|
||||
count += (process_mask & 1);
|
||||
process_mask >>= 1;
|
||||
}
|
||||
return std::max(1u, count);
|
||||
}
|
||||
return std::max(1u, std::thread::hardware_concurrency());
|
||||
#else
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
if (sched_getaffinity(0, sizeof(set), &set) == 0) {
|
||||
return std::max(1, CPU_COUNT(&set));
|
||||
}
|
||||
return std::max(1u, std::thread::hardware_concurrency());
|
||||
#endif
|
||||
}
|
||||
|
||||
/// The calling thread only runs on otherwise idle cores (falls back to the lowest nice level).
|
||||
inline void MakeBackground() {
|
||||
#ifdef _WIN32
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE);
|
||||
#else
|
||||
sched_param param{};
|
||||
if (sched_setscheduler(0, SCHED_IDLE, ¶m) != 0) {
|
||||
setpriority(PRIO_PROCESS, static_cast<id_t>(gettid()), 19);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace BbThreads
|
||||
@@ -7,9 +7,9 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "bbport_threads.h"
|
||||
|
||||
extern "C" std::uint64_t runtime_disabled_optimizations;
|
||||
/// Recovery point for speculative guest memory reads on this thread (runtime_memory.c).
|
||||
extern "C" __thread sigjmp_buf* runtime_fault_recover;
|
||||
|
||||
namespace BbToggle {
|
||||
enum : std::uint64_t {
|
||||
@@ -62,6 +62,9 @@ enum : std::uint64_t {
|
||||
// when nothing moves. Static-camera flicker of railings/window bars p99.9 -45% (2026-10-03).
|
||||
TaaKeepNearerHistory = 1ull << 55,
|
||||
SceneMipBias = 1ull << 57, ///< negative LOD bias of G-buffer samplers at reduced scene sizes
|
||||
/// Scene textures use BB_ANISO (16) times anisotropic filtering instead of the game's
|
||||
/// ratio; bit set: the game's own samplers (A/B while the game runs).
|
||||
ForcedAniso = 1ull << 61,
|
||||
// Bits 20-29 are used as raw debug toggles by the camera/object motion and the upscaler.
|
||||
};
|
||||
inline bool Disabled(std::uint64_t bit) {
|
||||
@@ -78,7 +81,7 @@ inline std::atomic<std::uint64_t> images_registered{0};
|
||||
inline std::atomic<std::uint64_t> image_upload_bytes{0};
|
||||
inline std::atomic<std::uint64_t> buffer_upload_bytes{0};
|
||||
inline std::atomic<int> gpu_thread_clock{-1}; ///< clockid_t of the GPU command thread
|
||||
inline std::atomic<std::uint64_t> draws{0}, dispatches{0}, submissions{0};
|
||||
inline std::atomic<std::uint64_t> draws{0}, dispatches{0}, submissions{0}, flips{0};
|
||||
/// Frames the GPU command thread has started (display pass), for per-frame diagnostics.
|
||||
inline std::atomic<std::uint64_t> gpu_frames{0};
|
||||
/// Wall time spent in operations suspected of stalls (ns, all threads).
|
||||
@@ -112,6 +115,12 @@ inline std::atomic<std::uint64_t> reduced_draws{0}, scene_draws{0};
|
||||
/// for host copies before a submission or fence, and for GPU ticks.
|
||||
inline std::atomic<std::uint64_t> sync_recording_ns{0}, host_copies_wait_ns{0}, tick_wait_ns{0},
|
||||
copy_threads_wait_ns{0}, host_copy_waits{0};
|
||||
/// The texture cache collector: the usage it compares, the mark it starts at, images it freed.
|
||||
inline std::atomic<std::uint64_t> gc_used_bytes{0}, gc_trigger_bytes{0}, gc_freed_images{0};
|
||||
/// Seconds of a steady clock, updated at every guest submission (cheap ages for caches).
|
||||
inline std::atomic<std::uint32_t> coarse_second{0};
|
||||
/// Images the collector passed over (GPU-written, not evictable without memory pressure).
|
||||
inline std::atomic<std::uint64_t> gc_kept_images{0};
|
||||
struct WaitTimer {
|
||||
std::atomic<std::uint64_t>& total;
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
@@ -6,8 +6,15 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <x86intrin.h>
|
||||
|
||||
namespace BbWriteLog {
|
||||
@@ -56,7 +63,11 @@ 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 _WIN32
|
||||
static thread_local const std::uint32_t tid = static_cast<std::uint32_t>(GetCurrentThreadId());
|
||||
#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);
|
||||
@@ -81,15 +92,21 @@ void Record(std::uint64_t address, const void* data, std::uint64_t size, Source
|
||||
|
||||
extern "C" void bbgpu_dump_guest_writes(void* ucontext) {
|
||||
using namespace BbWriteLog;
|
||||
if (Mode() == 0) {
|
||||
if (Mode() == 0 || !ucontext) {
|
||||
return;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
const auto* ctx = static_cast<const CONTEXT*>(ucontext);
|
||||
const std::uint64_t regs[] = {ctx->Rax, ctx->Rbx, ctx->Rcx, ctx->Rdx,
|
||||
ctx->Rsi, ctx->Rdi, ctx->R14, ctx->R15};
|
||||
#else
|
||||
const auto* uc = static_cast<const ucontext_t*>(ucontext);
|
||||
const auto* g = uc->uc_mcontext.gregs;
|
||||
const std::uint64_t regs[] = {std::uint64_t(g[REG_RAX]), std::uint64_t(g[REG_RBX]),
|
||||
std::uint64_t(g[REG_RCX]), std::uint64_t(g[REG_RDX]),
|
||||
std::uint64_t(g[REG_RSI]), std::uint64_t(g[REG_RDI]),
|
||||
std::uint64_t(g[REG_R14]), std::uint64_t(g[REG_R15])};
|
||||
#endif
|
||||
const char* names[] = {"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "r14", "r15"};
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
std::fprintf(stderr, "Write log: %s=%#llx\n", names[i], (unsigned long long)regs[i]);
|
||||
@@ -140,11 +157,11 @@ extern "C" void bbgpu_dump_guest_writes(void* ucontext) {
|
||||
int shown = 0;
|
||||
for (std::uint64_t i = n; i-- > (n > Size ? n - Size : 0) && shown < 64;) {
|
||||
const Entry& e = ring[i % Size];
|
||||
bool near = false;
|
||||
bool is_near = false;
|
||||
for (const auto r : regs) {
|
||||
near |= r + 0x1000 > e.address && r < e.address + e.size + 0x1000;
|
||||
is_near |= r + 0x1000 > e.address && r < e.address + e.size + 0x1000;
|
||||
}
|
||||
if (near) {
|
||||
if (is_near) {
|
||||
print(e, "near");
|
||||
++shown;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// bbport: host threads that may call guest code (AvPlayer allocator callbacks).
|
||||
// Each thread gets a guest TCB (GS base, TLS) from the C runtime before running.
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <stop_token>
|
||||
#include <thread>
|
||||
#include "common/types.h"
|
||||
@@ -14,26 +13,43 @@ public:
|
||||
Thread() = default;
|
||||
~Thread() { Stop(); }
|
||||
void Run(std::function<void(std::stop_token)>&& func) {
|
||||
Stop();
|
||||
std::scoped_lock lock{mutex};
|
||||
thread = std::jthread([func = std::move(func)](std::stop_token stop) {
|
||||
runtime_thread_attach_host("bb:hle");
|
||||
func(stop);
|
||||
});
|
||||
}
|
||||
// A thread may stop its own Thread object (AvPlayer does); it detaches instead of joining.
|
||||
void Join() {
|
||||
if (!thread.joinable()) return;
|
||||
if (thread.get_id() == std::this_thread::get_id()) thread.detach();
|
||||
else thread.join();
|
||||
// AvPlayer threads stop their own Thread objects while the owner may be stopping them too
|
||||
// (AvPlayerSource::Stop). The jthread is taken out under the lock, so exactly one caller
|
||||
// ends it: the thread itself detaches, anyone else joins.
|
||||
void Join() { Finish(Take()); }
|
||||
bool Joinable() const {
|
||||
std::scoped_lock lock{mutex};
|
||||
return thread.joinable();
|
||||
}
|
||||
bool RequestStop() {
|
||||
std::scoped_lock lock{mutex};
|
||||
return thread.request_stop();
|
||||
}
|
||||
bool Joinable() const { return thread.joinable(); }
|
||||
void Stop() {
|
||||
if (thread.joinable()) {
|
||||
thread.request_stop();
|
||||
Join();
|
||||
}
|
||||
std::jthread taken = Take();
|
||||
if (taken.joinable()) taken.request_stop();
|
||||
Finish(std::move(taken));
|
||||
}
|
||||
|
||||
private:
|
||||
std::jthread Take() {
|
||||
std::scoped_lock lock{mutex};
|
||||
return std::move(thread);
|
||||
}
|
||||
static void Finish(std::jthread taken) {
|
||||
if (!taken.joinable()) return;
|
||||
if (taken.get_id() == std::this_thread::get_id()) taken.detach();
|
||||
else taken.join();
|
||||
}
|
||||
|
||||
mutable std::mutex mutex;
|
||||
std::jthread thread;
|
||||
};
|
||||
} // namespace Libraries::Kernel
|
||||
@@ -31,6 +31,10 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
|
||||
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
const SDL_PropertiesID wp = SDL_GetWindowProperties(window);
|
||||
#if defined(_WIN32)
|
||||
window_info.type = WindowSystemType::Windows;
|
||||
window_info.render_surface = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
|
||||
#else
|
||||
if (driver && !std::strcmp(driver, "x11")) {
|
||||
window_info.type = WindowSystemType::X11;
|
||||
window_info.display_connection = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);
|
||||
@@ -42,6 +46,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
|
||||
} else {
|
||||
UNREACHABLE_MSG("Unsupported SDL video driver {}", driver ? driver : "(none)");
|
||||
}
|
||||
#endif
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSizeInPixels(window, &w, &h);
|
||||
width = w;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
set "PYTHON_EXE="
|
||||
for %%P in (
|
||||
"C:\Program Files\Python313\python.exe"
|
||||
"C:\Program Files\Python312\python.exe"
|
||||
"C:\Program Files\Python311\python.exe"
|
||||
"C:\Program Files\Python310\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python313\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python312\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python311\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python310\python.exe"
|
||||
) do (
|
||||
if not defined PYTHON_EXE if exist %%P set "PYTHON_EXE=%%~P"
|
||||
)
|
||||
|
||||
if not defined PYTHON_EXE (
|
||||
for /f "delims=" %%I in ('where python.exe 2^>nul') do (
|
||||
echo "%%~I" | findstr /i /c:"WindowsApps" >nul
|
||||
if errorlevel 1 (
|
||||
if not defined PYTHON_EXE set "PYTHON_EXE=%%~I"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if not defined PYTHON_EXE (
|
||||
echo [ERROR] Python not found. Please install Python 3.10+ and add it to PATH.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
start "" "%PYTHON_EXE%" launcher.py
|
||||
+1074
File diff suppressed because it is too large.
Load diff
@@ -3435,4 +3435,9 @@
|
||||
<Line Type="bytes" Address="0x01de368b" Value="c3" />
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="Bloodborne" Name="Sprint Fix (High FPS)" Note="Fixes sprint dropping to half speed above ~147 FPS (and at random moments above ~90 FPS). Use together with Uncap FPS++ or 90 FPS++. The game's wall detector measured distance per frame (a 30 FPS rule); this makes it measure speed instead." Author="yogesh1239" PatchVer="1.0" AppVer="01.09" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x01914c24" Value="c5fa5e458c0f1f00"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
</Patch>
|
||||
@@ -0,0 +1,247 @@
|
||||
diff --git a/src/ffx_vk_fsr4_v07.c b/src/ffx_vk_fsr4_v07.c
|
||||
index c30f7cf..eb42c16 100644
|
||||
--- a/src/ffx_vk_fsr4_v07.c
|
||||
+++ b/src/ffx_vk_fsr4_v07.c
|
||||
@@ -35,6 +35,7 @@
|
||||
#define NUM_POOL_FRAMES 3 /* maximum unretired host dispatches */
|
||||
#define MAX_STAGING 32
|
||||
#define MAX_EXTERNAL_IMAGES 8 /* color, depth, motion, output + host slack */
|
||||
+#define PROF_MAX_JOBS 32 /* timestamped dispatches per frame */
|
||||
#define CBUF_FRAME_BYTES (512 * 1024)
|
||||
#define CBUF_RING_BYTES (CBUF_FRAME_BYTES * NUM_POOL_FRAMES)
|
||||
|
||||
@@ -182,6 +183,16 @@ struct FfxFsr4VkContext {
|
||||
VkBool32 int8Supported;
|
||||
VkBool32 dotProductSupported;
|
||||
VkBool32 computeDerivativesSupported;
|
||||
+
|
||||
+ /* bbport: BB_FSR4_PROFILE=1 — GPU time per pass (timestamps around each
|
||||
+ * dispatch, read back when the frame retires, printed every 300 frames). */
|
||||
+ VkQueryPool profPool;
|
||||
+ float profPeriodNs;
|
||||
+ uint32_t profCount[NUM_POOL_FRAMES];
|
||||
+ uint32_t profPass[NUM_POOL_FRAMES][PROF_MAX_JOBS];
|
||||
+ double profMs[MAX_PIPELINES];
|
||||
+ uint32_t profCalls[MAX_PIPELINES];
|
||||
+ uint64_t profFrames;
|
||||
};
|
||||
|
||||
static FfxErrorCode restoreExternalImages(FfxFsr4VkContext* c,
|
||||
@@ -625,6 +636,36 @@ static VkFormat toVkFmt(FfxSurfaceFormat f)
|
||||
|
||||
// ── pipeline creation ─────────────────────────────────────────────────────────
|
||||
|
||||
+/* bbport: BB_FSR4_STATS=1 (the device must enable pipelineExecutableInfo) prints the
|
||||
+ * driver's statistics of each pass: registers, spills, instruction counts. */
|
||||
+static void printPipelineStats(FfxFsr4VkContext* c, uint32_t passIdx, VkPipeline pipeline)
|
||||
+{
|
||||
+ PFN_vkGetPipelineExecutableStatisticsKHR getStats =
|
||||
+ (PFN_vkGetPipelineExecutableStatisticsKHR)vkGetDeviceProcAddr(
|
||||
+ c->dev, "vkGetPipelineExecutableStatisticsKHR");
|
||||
+ if (!getStats) return;
|
||||
+ VkPipelineExecutableInfoKHR info;
|
||||
+ memset(&info, 0, sizeof(info));
|
||||
+ info.sType = VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_INFO_KHR;
|
||||
+ info.pipeline = pipeline;
|
||||
+ uint32_t count = 0;
|
||||
+ if (getStats(c->dev, &info, &count, NULL) != VK_SUCCESS || count == 0) return;
|
||||
+ VkPipelineExecutableStatisticKHR* s = calloc(count, sizeof(*s));
|
||||
+ if (!s) return;
|
||||
+ for (uint32_t i = 0; i < count; ++i) s[i].sType = VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_STATISTIC_KHR;
|
||||
+ if (getStats(c->dev, &info, &count, s) == VK_SUCCESS) {
|
||||
+ printf("FSR 4 pass %u:", passIdx);
|
||||
+ for (uint32_t i = 0; i < count; ++i) {
|
||||
+ if (s[i].format == VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_UINT64_KHR)
|
||||
+ printf(" %s=%llu", s[i].name, (unsigned long long)s[i].value.u64);
|
||||
+ else if (s[i].format == VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_BOOL32_KHR)
|
||||
+ printf(" %s=%u", s[i].name, s[i].value.b32);
|
||||
+ }
|
||||
+ printf("\n");
|
||||
+ }
|
||||
+ free(s);
|
||||
+}
|
||||
+
|
||||
static VkResult mkPipeline(FfxFsr4VkContext* c, uint32_t passIdx, VkPipe* out)
|
||||
{
|
||||
if (!c || !out || passIdx >= FFX_FSR4_VK_PASS_COUNT)
|
||||
@@ -709,7 +750,11 @@ static VkResult mkPipeline(FfxFsr4VkContext* c, uint32_t passIdx, VkPipe* out)
|
||||
cpci.stage.pName=blob->entryPoint ? blob->entryPoint : "main";
|
||||
cpci.layout=out->layout;
|
||||
|
||||
+ const char* statsEnv = getenv("BB_FSR4_STATS");
|
||||
+ const int stats = statsEnv && statsEnv[0] == '1';
|
||||
+ if (stats) cpci.flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
|
||||
r = vkCreateComputePipelines(c->dev, VK_NULL_HANDLE, 1, &cpci, c->alloc, &out->pipeline);
|
||||
+ if (stats && r == VK_SUCCESS) printPipelineStats(c, passIdx, out->pipeline);
|
||||
vkDestroyShaderModule(c->dev, sm, c->alloc);
|
||||
if (r) {
|
||||
vkDestroyPipelineLayout(c->dev, out->layout, c->alloc);
|
||||
@@ -1433,6 +1478,38 @@ static FfxErrorCode writeFullDs(FfxFsr4VkContext* c, const VkPipe* pipe, VkDescr
|
||||
|
||||
// ── execute all queued jobs ───────────────────────────────────────────────────
|
||||
|
||||
+static void profCollect(FfxFsr4VkContext* c, uint32_t slot)
|
||||
+{
|
||||
+ const uint32_t count = c->profCount[slot];
|
||||
+ if (!c->profPool || count == 0) return;
|
||||
+ c->profCount[slot] = 0;
|
||||
+ uint64_t ts[PROF_MAX_JOBS + 1];
|
||||
+ if (vkGetQueryPoolResults(c->dev, c->profPool, slot * (PROF_MAX_JOBS + 1), count + 1,
|
||||
+ sizeof(ts), ts, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT) != VK_SUCCESS)
|
||||
+ return;
|
||||
+ for (uint32_t j = 0; j < count; ++j) {
|
||||
+ const uint32_t pass = c->profPass[slot][j];
|
||||
+ c->profMs[pass] += (double)(ts[j + 1] - ts[j]) * c->profPeriodNs * 1e-6;
|
||||
+ c->profCalls[pass]++;
|
||||
+ }
|
||||
+ if (++c->profFrames % 300 != 0) return;
|
||||
+ static const char* names[] = {"pre", "model 1", "model 2", "model 3", "model 4",
|
||||
+ "model 5", "model 6", "model 7", "model 8", "model 9", "model 10", "model 11",
|
||||
+ "model 12", "post", "rcas", "spd auto exposure"};
|
||||
+ double total = 0.0;
|
||||
+ for (uint32_t p = 0; p < MAX_PIPELINES; ++p) total += c->profMs[p];
|
||||
+ printf("FSR 4 profile: %.3f ms/frame over %llu frames\n", total / 300.0,
|
||||
+ (unsigned long long)300);
|
||||
+ for (uint32_t p = 0; p < MAX_PIPELINES; ++p) {
|
||||
+ if (!c->profCalls[p]) continue;
|
||||
+ printf(" %6.3f ms/frame %s\n", c->profMs[p] / 300.0,
|
||||
+ p < sizeof(names) / sizeof(names[0]) ? names[p] : "?");
|
||||
+ c->profMs[p] = 0.0;
|
||||
+ c->profCalls[p] = 0;
|
||||
+ }
|
||||
+ fflush(stdout);
|
||||
+}
|
||||
+
|
||||
static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32 id) {
|
||||
(void)id;
|
||||
if (!I || !I->device) return FFX_ERROR_INVALID_POINTER;
|
||||
@@ -1503,6 +1580,12 @@ static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32
|
||||
* Move them to the provider's GENERAL compute ABI only after all resource
|
||||
* registration has succeeded, then restore them during unregister. */
|
||||
transitionExternalImagesToCompute(c, cmd);
|
||||
+ const uint32_t profBase = cur * (PROF_MAX_JOBS + 1);
|
||||
+ uint32_t profJobs = 0;
|
||||
+ if (c->profPool) {
|
||||
+ vkCmdResetQueryPool(cmd, c->profPool, profBase, PROF_MAX_JOBS + 1);
|
||||
+ vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, c->profPool, profBase);
|
||||
+ }
|
||||
|
||||
for (uint32_t ji=0; ji<c->jobCount; ji++) {
|
||||
FfxGpuJobDescription* job=&c->jobs[ji].d;
|
||||
@@ -1582,6 +1665,11 @@ static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32
|
||||
bp->layout,0,1,&ds,0,NULL);
|
||||
|
||||
vkCmdDispatch(cmd,cj->dimensions[0],cj->dimensions[1],cj->dimensions[2]);
|
||||
+ if (c->profPool && profJobs < PROF_MAX_JOBS) {
|
||||
+ vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c->profPool,
|
||||
+ profBase + 1 + profJobs);
|
||||
+ c->profPass[cur][profJobs++] = passIdx;
|
||||
+ }
|
||||
|
||||
// Memory barrier between compute passes — ensures writes from this
|
||||
// dispatch are visible to the next dispatch (pre→model→post chain).
|
||||
@@ -1610,6 +1698,7 @@ static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32
|
||||
} else EXECUTE_FAIL(FFX_ERROR_INVALID_ENUM);
|
||||
}
|
||||
|
||||
+ c->profCount[cur] = profJobs;
|
||||
c->jobCount=0;
|
||||
c->recordingFrame = UINT32_MAX;
|
||||
#undef EXECUTE_FAIL
|
||||
@@ -1694,6 +1783,21 @@ VkResult ffxFsr4VkCreateContext(const FfxFsr4VkCreateInfo* ci, FfxInterface* out
|
||||
sci.addressModeU=sci.addressModeV=sci.addressModeW=VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sci.maxLod=VK_LOD_CLAMP_NONE;
|
||||
r=vkCreateSampler(c->dev,&sci,c->alloc,&c->linearSampler);
|
||||
+ {
|
||||
+ const char* prof = getenv("BB_FSR4_PROFILE");
|
||||
+ if (r == VK_SUCCESS && prof && prof[0] == '1') {
|
||||
+ VkPhysicalDeviceProperties props;
|
||||
+ vkGetPhysicalDeviceProperties(c->phys, &props);
|
||||
+ c->profPeriodNs = props.limits.timestampPeriod;
|
||||
+ VkQueryPoolCreateInfo qci;
|
||||
+ memset(&qci, 0, sizeof(qci));
|
||||
+ qci.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||
+ qci.queryType = VK_QUERY_TYPE_TIMESTAMP;
|
||||
+ qci.queryCount = NUM_POOL_FRAMES * (PROF_MAX_JOBS + 1);
|
||||
+ if (vkCreateQueryPool(c->dev, &qci, c->alloc, &c->profPool) != VK_SUCCESS)
|
||||
+ c->profPool = VK_NULL_HANDLE;
|
||||
+ }
|
||||
+ }
|
||||
if (r) goto fail;
|
||||
|
||||
// cbuffer ring buffer (host-visible, persistently mapped)
|
||||
@@ -1809,6 +1913,10 @@ void ffxFsr4VkDestroyContext(FfxFsr4VkContext* c)
|
||||
}
|
||||
memset(c->frame, 0, sizeof(c->frame));
|
||||
c->recordingFrame = UINT32_MAX;
|
||||
+ if (c->profPool) {
|
||||
+ vkDestroyQueryPool(c->dev, c->profPool, c->alloc);
|
||||
+ c->profPool = VK_NULL_HANDLE;
|
||||
+ }
|
||||
if (c->linearSampler) {
|
||||
vkDestroySampler(c->dev,c->linearSampler,c->alloc);
|
||||
c->linearSampler = VK_NULL_HANDLE;
|
||||
@@ -1853,6 +1961,7 @@ VkResult ffxFsr4VkRetireFrame(FfxInterface* iface, uint64_t completedFrameId)
|
||||
for (uint32_t i = 0; i < NUM_POOL_FRAMES; ++i) {
|
||||
FrameLifetime* frame = &c->frame[i];
|
||||
if (!frame->inUse || frame->frameId > completedFrameId) continue;
|
||||
+ profCollect(c, i);
|
||||
if (vkResetDescriptorPool(c->dev, c->pool[i], 0) != VK_SUCCESS)
|
||||
return VK_ERROR_DEVICE_LOST;
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
diff --git a/upstream/ffx-1.1.4/sdk/include/FidelityFX/host/ffx_types.h b/upstream/ffx-1.1.4/sdk/include/FidelityFX/host/ffx_types.h
|
||||
index f7625c2..9564f3b 100644
|
||||
--- a/upstream/ffx-1.1.4/sdk/include/FidelityFX/host/ffx_types.h
|
||||
+++ b/upstream/ffx-1.1.4/sdk/include/FidelityFX/host/ffx_types.h
|
||||
@@ -23,6 +23,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
+#include <string.h>
|
||||
+#include <wchar.h>
|
||||
+#ifdef __cplusplus
|
||||
+#include <cstring>
|
||||
+#include <cwchar>
|
||||
+#endif
|
||||
|
||||
///
|
||||
/// @defgroup ffxSDK SDK
|
||||
diff --git a/upstream/ffx-1.1.4/sdk/src/backends/vk/ffx_vk.cpp b/upstream/ffx-1.1.4/sdk/src/backends/vk/ffx_vk.cpp
|
||||
index b2439b6..71d5369 100644
|
||||
--- a/upstream/ffx-1.1.4/sdk/src/backends/vk/ffx_vk.cpp
|
||||
+++ b/upstream/ffx-1.1.4/sdk/src/backends/vk/ffx_vk.cpp
|
||||
@@ -34,6 +34,8 @@
|
||||
#endif // _WIN32
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
+#include <new>
|
||||
+#include <cmath>
|
||||
|
||||
// prototypes for functions in the interface
|
||||
FfxVersionNumber GetSDKVersionVK(FfxInterface* backendInterface);
|
||||
diff --git a/upstream/ffx-1.1.4/sdk/src/shared/ffx_message.cpp b/upstream/ffx-1.1.4/sdk/src/shared/ffx_message.cpp
|
||||
index e325a05..a31da46 100644
|
||||
--- a/upstream/ffx-1.1.4/sdk/src/shared/ffx_message.cpp
|
||||
+++ b/upstream/ffx-1.1.4/sdk/src/shared/ffx_message.cpp
|
||||
@@ -28,6 +28,8 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h> // required for OutputDebugString()
|
||||
+#include <wchar.h>
|
||||
+#include <stdio.h>
|
||||
#endif // #ifdef _WIN32
|
||||
|
||||
static ffxMessageCallback s_messageCallback;
|
||||
@@ -48,10 +50,10 @@ void ffxPrintMessage(uint32_t type, const wchar_t* message)
|
||||
// Format the message string
|
||||
wchar_t buffer[512];
|
||||
if (type == FFX_MESSAGE_TYPE_ERROR) {
|
||||
- swprintf_s(buffer, 512, L"FSR_API_DEBUG_ERROR: %ls\n", message);
|
||||
+ swprintf(buffer, 512, L"FSR_API_DEBUG_ERROR: %ls\n", message);
|
||||
}
|
||||
else if (type == FFX_MESSAGE_TYPE_WARNING) {
|
||||
- swprintf_s(buffer, 512, L"FSR_API_DEBUG_WARNING: %ls\n", message);
|
||||
+ swprintf(buffer, 512, L"FSR_API_DEBUG_WARNING: %ls\n", message);
|
||||
}
|
||||
OutputDebugStringW(buffer);
|
||||
} else {
|
||||
@@ -0,0 +1,173 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
REM Find Python 3
|
||||
if "%PYTHON%"=="" (
|
||||
for %%P in (
|
||||
"C:\Program Files\Python313\python.exe"
|
||||
"C:\Program Files\Python312\python.exe"
|
||||
"C:\Program Files\Python311\python.exe"
|
||||
"C:\Program Files\Python310\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python313\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python312\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python311\python.exe"
|
||||
"%LocalAppData%\Programs\Python\Python310\python.exe"
|
||||
) do (
|
||||
if not defined PYTHON if exist %%P set "PYTHON=%%~P"
|
||||
)
|
||||
)
|
||||
if "%PYTHON%"=="" (
|
||||
for /f "delims=" %%i in ('where python.exe 2^>nul') do (
|
||||
set "pycand=%%i"
|
||||
if not "%%~dpi"=="%LocalAppData%\Microsoft\WindowsApps\" (
|
||||
if "%PYTHON%"=="" set "PYTHON=%%i"
|
||||
)
|
||||
)
|
||||
)
|
||||
if "%PYTHON%"=="" (
|
||||
for /f "tokens=*" %%i in ('where py.exe 2^>nul') do (
|
||||
if "%PYTHON%"=="" set "PYTHON=%%i"
|
||||
)
|
||||
)
|
||||
if "%PYTHON%"=="" set "PYTHON=python"
|
||||
|
||||
REM Writable directory for generated files (out/), saves (user/) and bbport.ini
|
||||
if "%BB_DATA_DIR%"=="" set "BB_DATA_DIR=."
|
||||
set "out=%BB_DATA_DIR%\out"
|
||||
if not exist "%out%" mkdir "%out%"
|
||||
if "%BB_CONFIG%"=="" set "BB_CONFIG=%BB_DATA_DIR%\bbport.ini"
|
||||
|
||||
REM Game directory containing eboot.bin
|
||||
set "EXTRA_ARGS="
|
||||
:arg_loop
|
||||
if "%~1"=="" goto arg_done
|
||||
if exist "%~1\eboot.bin" (
|
||||
set "BB_GAME_DIR=%~1"
|
||||
shift
|
||||
goto arg_loop
|
||||
)
|
||||
if "%~nx1"=="eboot.bin" if exist "%~1" (
|
||||
set "BB_GAME_DIR=%~dp1"
|
||||
shift
|
||||
goto arg_loop
|
||||
)
|
||||
set EXTRA_ARGS=%EXTRA_ARGS% %1
|
||||
shift
|
||||
goto arg_loop
|
||||
:arg_done
|
||||
|
||||
if "%BB_GAME_DIR%"=="" set "BB_GAME_DIR=..\CUSA03173"
|
||||
set "game=%BB_GAME_DIR%"
|
||||
REM Strip trailing backslash if present
|
||||
if "%game:~-1%"=="\" set "game=%game:~0,-1%"
|
||||
if not exist "%game%\eboot.bin" (
|
||||
echo No eboot.bin in %game% ^(set BB_GAME_DIR or pass path to run.bat^). >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "mods_dir=%BB_MODS_DIR%"
|
||||
if "%mods_dir%"=="" set "mods_dir=%BB_DATA_DIR%\mods"
|
||||
set "mods_config=%BB_MODS_CONFIG%"
|
||||
if "%mods_config%"=="" set "mods_config=%BB_DATA_DIR%\mods.json"
|
||||
set "mods_enabled=%BB_MODS_ENABLED%"
|
||||
if "%mods_enabled%"=="" set "mods_enabled=1"
|
||||
|
||||
REM NOTE: for /f ('"exe" args "more args"') trips cmd's quote-stripping rule and prints
|
||||
REM "The filename, directory name, or volume label syntax is incorrect." So write the
|
||||
REM output to a temp file and read that instead (last line wins, same as before).
|
||||
set "mods_result=%out%\mods_game.txt"
|
||||
if exist "%mods_result%" del "%mods_result%"
|
||||
"%PYTHON%" scripts\mods.py "%game%" --out "%out%" --mods-dir "%mods_dir%" --config "%mods_config%" --enabled "%mods_enabled%" > "%mods_result%"
|
||||
if exist "%mods_result%" for /f "usebackq delims=" %%g in ("%mods_result%") do set "game=%%g"
|
||||
|
||||
"%PYTHON%" scripts\prepare.py "%game%" --out "%out%"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
"%PYTHON%" scripts\link_libc.py "%game%" --out "%out%"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
"%PYTHON%" scripts\link_modules.py "%game%" --out "%out%"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
set "sku=%BB_CONTENT_SKU%"
|
||||
if "%sku%"=="" set "sku=full"
|
||||
"%PYTHON%" scripts\content_profile.py "%game%" --out "%out%" --sku "%sku%"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
if "%BB_AUTO_RENDER_RES%"=="1" (
|
||||
set "BB_RENDER_RES="
|
||||
set "BB_OUTPUT_RES="
|
||||
set "BB_AUTO_RENDER_RES="
|
||||
)
|
||||
|
||||
if "%BB_FPS%"=="" set "BB_FPS=uncap"
|
||||
set "fps=%BB_FPS%"
|
||||
|
||||
set "scaled_render="
|
||||
set "scaled_output="
|
||||
if "%BB_RENDER_RES%"=="" (
|
||||
for /f "tokens=1,2" %%a in ('"%PYTHON%" scripts\patches.py --print-scaled --settings "%BB_CONFIG%" 2^>nul') do (
|
||||
set "scaled_render=%%a"
|
||||
set "scaled_output=%%b"
|
||||
)
|
||||
)
|
||||
|
||||
set "live=0"
|
||||
if not "%scaled_output%"=="" (
|
||||
if not "%BB_LIVE_RES%"=="" set "live=%BB_LIVE_RES%"
|
||||
if "%live%"=="0" if exist "%BB_CONFIG%" (
|
||||
for /f "tokens=1,2 delims==" %%a in ('type "%BB_CONFIG%" 2^>nul') do (
|
||||
if "%%a"=="live_resolution" set "live=%%b"
|
||||
)
|
||||
)
|
||||
if "%live%"=="auto" (
|
||||
set "caps=out\bb-gpu-capabilities.exe"
|
||||
for /f "delims=" %%c in ('"%caps%" --live-resolution 2^>nul') do set "live=%%c"
|
||||
)
|
||||
if not "%live%"=="1" set "live=0"
|
||||
)
|
||||
|
||||
if "%live%"=="1" (
|
||||
echo Output %scaled_output%: live resolution changes ^(live_resolution=0: startup patch^)
|
||||
) else if not "%scaled_output%"=="" (
|
||||
set "BB_RENDER_RES=%scaled_render%"
|
||||
set "BB_OUTPUT_RES=%scaled_output%"
|
||||
set "BB_AUTO_RENDER_RES=1"
|
||||
if "%BB_DMEM_MB%"=="" set "BB_DMEM_MB=9152"
|
||||
echo Output %scaled_output%: scene %scaled_render%, direct memory %BB_DMEM_MB% MiB ^(live_resolution=1: live changes^)
|
||||
)
|
||||
|
||||
set "patches_dir=%BB_PATCHES_DIR%"
|
||||
if "%patches_dir%"=="" set "patches_dir=%BB_DATA_DIR%\patches"
|
||||
set "patches_config=%BB_PATCHES_CONFIG%"
|
||||
if "%patches_config%"=="" set "patches_config=%BB_DATA_DIR%\patches.json"
|
||||
|
||||
"%PYTHON%" scripts\patches.py --out "%out%" --fps "%fps%" --extra "%BB_PATCHES%" --settings "%BB_CONFIG%" --game-dir "%game%" --render-res "%BB_RENDER_RES%" --output-res "%BB_OUTPUT_RES%" --patches-dir "%patches_dir%" --patches-config "%patches_config%"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
if "%BB_VBLANK_HZ%"=="" (
|
||||
if "%fps%"=="uncap" (
|
||||
set "BB_VBLANK_HZ=0"
|
||||
) else if "%fps%"=="90" (
|
||||
set "BB_VBLANK_HZ=90"
|
||||
) else (
|
||||
set "BB_VBLANK_HZ=60"
|
||||
)
|
||||
)
|
||||
|
||||
if not exist "out\bbport.exe" (
|
||||
call build.bat
|
||||
if errorlevel 1 exit /b 1
|
||||
)
|
||||
|
||||
set "probe=out\bbport.exe"
|
||||
set "user_dir=%BB_USER_DIR%"
|
||||
if "%user_dir%"=="" set "user_dir=%BB_DATA_DIR%\user"
|
||||
set "timeout=%BB_TIMEOUT%"
|
||||
if "%timeout%"=="" set "timeout=0"
|
||||
|
||||
echo Launching Bloodborne PC (%probe%)...
|
||||
"%probe%" "%out%\boot-linked.bin" --content-profile "%out%\content.bin" --patches "%out%\patches.bin" --app0 "%game%" --user "%user_dir%" --timeout "%timeout%" %EXTRA_ARGS%
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The game files bbport runs: Bloodborne CUSA03173 with the 1.09 update merged in.
|
||||
|
||||
Other versions start and then fail inside the game's code (the base game 1.00 faults at guest
|
||||
offset 0x20348b8): hooks and patches use the addresses of this one executable. The check compares
|
||||
the loaded executable image, whose hash is the same whatever tool dumped it (the SELF headers
|
||||
around it differ). BB_SKIP_GAME_CHECK=1 skips it.
|
||||
"""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
SUPPORTED_TITLE = 'CUSA03173'
|
||||
SUPPORTED_VERSION = '01.09'
|
||||
SUPPORTED_IMAGE = '071df19c8880086d97182dbc057bc8cb37badaca57d9112683836b24a0444c0a'
|
||||
|
||||
|
||||
def image_sha256(game):
|
||||
"""The SHA-256 of eboot.bin's loaded image (its loadable segments at their addresses)."""
|
||||
from prepare import parse_self, span # same directory
|
||||
elf, header, ph, _segments, _missing = parse_self((Path(game) / 'eboot.bin').read_bytes())
|
||||
loads = [p for p in ph if p['type'] in (1, 0x61000010)]
|
||||
image = bytearray(max(p['vaddr'] + p['memsz'] for p in loads))
|
||||
for p in loads:
|
||||
image[p['vaddr']:p['vaddr'] + p['filesz']] = span(elf, p['offset'], p['filesz'])
|
||||
return hashlib.sha256(image).hexdigest()
|
||||
|
||||
|
||||
def problem(game, image_hash=None):
|
||||
"""None for the supported game, else (kind, title, version): kind is 'missing_update' (base
|
||||
game or an older update), 'wrong_eboot' (param.sfo says 1.09, eboot.bin is another version),
|
||||
'other_title' (another edition or region) or 'unreadable'."""
|
||||
if os.environ.get('BB_SKIP_GAME_CHECK') == '1':
|
||||
return None
|
||||
from prepare import sfo
|
||||
try:
|
||||
info = sfo((Path(game) / 'sce_sys/param.sfo').read_bytes())
|
||||
except (OSError, ValueError, IndexError):
|
||||
info = {}
|
||||
title, version = info.get('TITLE_ID', '?'), info.get('APP_VER', '?')
|
||||
try:
|
||||
if (image_hash or image_sha256(game)) == SUPPORTED_IMAGE:
|
||||
return None
|
||||
except (OSError, ValueError, IndexError, StopIteration, KeyError):
|
||||
return 'unreadable', title, version
|
||||
if title != SUPPORTED_TITLE:
|
||||
return 'other_title', title, version
|
||||
if version != SUPPORTED_VERSION:
|
||||
return 'missing_update', title, version
|
||||
return 'wrong_eboot', title, version
|
||||
|
||||
|
||||
def explain(kind, title, version):
|
||||
"""What is wrong and what to do, for the log (English)."""
|
||||
found = f'Found {title} version {version} (sce_sys/param.sfo).'
|
||||
return {
|
||||
'missing_update': f'{found} bbport needs the 1.09 update merged into the game folder: copy '
|
||||
'everything from the dumped 1.09 update (e.g. CUSA03173-patch) into the '
|
||||
'game folder, replacing files (eboot.bin and sce_sys too).',
|
||||
'wrong_eboot': f'{found} param.sfo is from 1.09 but eboot.bin is not: copy eboot.bin from '
|
||||
'the dumped 1.09 update into the game folder, replacing the old one.',
|
||||
'other_title': f'{found} Only Bloodborne {SUPPORTED_TITLE} with update 1.09 is supported '
|
||||
'for now; other editions and regions have a different executable.',
|
||||
'unreadable': f'{found} eboot.bin could not be read as a decrypted PS4 executable: dump '
|
||||
'the game and the 1.09 update again.',
|
||||
}[kind]
|
||||
+11
-9
@@ -8,6 +8,7 @@ import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
from prepare import parse_self, span, unpack
|
||||
|
||||
|
||||
@@ -163,15 +164,16 @@ def link(game, out):
|
||||
# segments are scanned, and only this exact 9-byte instruction.
|
||||
fs_load = bytes.fromhex('64488b042500000000')
|
||||
patched = 0
|
||||
for p in main['ph']:
|
||||
if p['type'] != 1 or not p['flags'] & 1:
|
||||
continue
|
||||
start, end = p['vaddr'], p['vaddr'] + p['filesz']
|
||||
at = image.find(fs_load, start, end)
|
||||
while at >= 0:
|
||||
image[at] = 0x65
|
||||
patched += 1
|
||||
at = image.find(fs_load, at + len(fs_load), end)
|
||||
if sys.platform != 'win32':
|
||||
for p in main['ph']:
|
||||
if p['type'] != 1 or not p['flags'] & 1:
|
||||
continue
|
||||
start, end = p['vaddr'], p['vaddr'] + p['filesz']
|
||||
at = image.find(fs_load, start, end)
|
||||
while at >= 0:
|
||||
image[at] = 0x65
|
||||
patched += 1
|
||||
at = image.find(fs_load, at + len(fs_load), end)
|
||||
main_tls = next((p for p in main['ph'] if p['type']==7), None)
|
||||
main_tls_values = (main_tls['vaddr'], main_tls['filesz'], main_tls['memsz'], main_tls['align']) if main_tls else (0,0,0,0)
|
||||
if main_tls and (main_tls['filesz'] > main_tls['memsz'] or main_tls['memsz'] > 1024*1024
|
||||
|
||||
@@ -15,6 +15,7 @@ import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
from prepare import parse_self, span, unpack
|
||||
from link_libc import encode_id
|
||||
|
||||
@@ -76,6 +77,8 @@ def module(path):
|
||||
|
||||
def patch_fs_loads(image, ph, base):
|
||||
"""Rewrite initial-exec `mov rax, fs:[0]` to GS: glibc owns FS on Linux."""
|
||||
if sys.platform == 'win32':
|
||||
return 0
|
||||
patched = 0
|
||||
for p in ph:
|
||||
if p['type'] != 1 or not p['flags'] & 1:
|
||||
|
||||
+20
-3
@@ -101,6 +101,23 @@ def mod_files(folder):
|
||||
yield relative, source
|
||||
|
||||
|
||||
def make_link(link_path, target_path, target_is_directory=False):
|
||||
try:
|
||||
link_path.symlink_to(target_path, target_is_directory=target_is_directory)
|
||||
except OSError:
|
||||
if sys.platform == 'win32':
|
||||
if target_is_directory:
|
||||
import _winapi
|
||||
_winapi.CreateJunction(str(target_path), str(link_path))
|
||||
else:
|
||||
try:
|
||||
os.link(str(target_path), str(link_path))
|
||||
except OSError:
|
||||
shutil.copy2(str(target_path), str(link_path))
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def expand(directory):
|
||||
"""Materialize one directory level; never write through a directory link."""
|
||||
if directory.is_symlink():
|
||||
@@ -110,7 +127,7 @@ def expand(directory):
|
||||
directory.unlink()
|
||||
directory.mkdir()
|
||||
for entry in target.iterdir():
|
||||
(directory / entry.name).symlink_to(entry, target_is_directory=entry.is_dir())
|
||||
make_link(directory / entry.name, entry, target_is_directory=entry.is_dir())
|
||||
elif directory.exists() and not directory.is_dir():
|
||||
raise ValueError(f'File/directory conflict at {directory.name}')
|
||||
else:
|
||||
@@ -138,7 +155,7 @@ def build_overlay(game, out, mods):
|
||||
overlay = Path(tempfile.mkdtemp(prefix='mod-game-', dir=out))
|
||||
try:
|
||||
for entry in game.iterdir():
|
||||
(overlay / entry.name).symlink_to(entry, target_is_directory=entry.is_dir())
|
||||
make_link(overlay / entry.name, entry, target_is_directory=entry.is_dir())
|
||||
replaced = added = 0
|
||||
for relative, source in replacements:
|
||||
# Each component takes the game's spelling when it exists in another case.
|
||||
@@ -154,7 +171,7 @@ def build_overlay(game, out, mods):
|
||||
raise ValueError(f'File/directory conflict: {relative}')
|
||||
else:
|
||||
added += 1
|
||||
destination.symlink_to(source)
|
||||
make_link(destination, source)
|
||||
print(f'Mods: {replaced} game files replaced, {added} added', file=sys.stderr)
|
||||
return overlay
|
||||
except BaseException:
|
||||
|
||||
+45
-4
@@ -14,7 +14,10 @@ from pathlib import Path
|
||||
|
||||
EBOOT_BASE=0x400000
|
||||
# BB_FPS presets: patch names from patches/Bloodborne.xml (app version 01.09).
|
||||
FPS_PRESETS={'30':[],'60':['60 FPS++'],'90':['90 FPS++'],'uncap':['Uncap FPS++']}
|
||||
# Above 60 FPS the sprint fix always goes with the frame rate patch: without it sprinting drops to
|
||||
# half speed (the game's wall detector measured distance per frame, a 30 FPS rule).
|
||||
FPS_PRESETS={'30':[],'60':['60 FPS++'],'90':['90 FPS++','Sprint Fix (High FPS)'],
|
||||
'uncap':['Uncap FPS++','Sprint Fix (High FPS)']}
|
||||
# Upscaler presets (bbport.ini "preset", the in-game menu): output / render size ratio. The game
|
||||
# then renders at 1920x1080 / ratio and the port's temporal upscaler restores the output size.
|
||||
OUTPUT_SIZE=(1920,1080)
|
||||
@@ -37,6 +40,34 @@ EFFECTS={
|
||||
'debug_camera':(None,'Restore Debug Camera'),
|
||||
'debug_menu':(None,'Restore Debug Menu (READ NOTES)'),
|
||||
}
|
||||
# Intel CPUs: the game's tone mapping turns black (DLC areas most; reported as darker than on the
|
||||
# PS4): the game code runs natively, and Intel's approximate float instructions differ from the
|
||||
# PS4's AMD CPU, so a block the game gates on a computed flag never runs. The community fix runs
|
||||
# it always; on by default on Intel. BB_INTEL_TONEMAP_FIX=0/1 forces it off or on.
|
||||
INTEL_TONEMAP='Intel Black Tonemap Fix'
|
||||
|
||||
|
||||
def intel_cpu(cpuinfo='/proc/cpuinfo'):
|
||||
if os.name == 'nt':
|
||||
proc_id = os.environ.get('PROCESSOR_IDENTIFIER', '')
|
||||
if 'Intel' in proc_id or 'GenuineIntel' in proc_id:
|
||||
return True
|
||||
try:
|
||||
import platform
|
||||
return 'Intel' in platform.processor()
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
with open(cpuinfo) as f:
|
||||
return any(line.startswith('vendor_id') and 'GenuineIntel' in line for line in f)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def intel_tonemap_fix(env=os.environ, cpuinfo='/proc/cpuinfo'):
|
||||
forced=env.get('BB_INTEL_TONEMAP_FIX')
|
||||
return forced=='1' if forced in ('0','1') else intel_cpu(cpuinfo)
|
||||
|
||||
# model_lod: -2 highest, 0 the game's, 1 lower, 2 lowest.
|
||||
MODEL_LOD={'-2':'Model LOD -2 (Highest)','1':'Model LOD 1 (Lower)','2':'Model LOD 2 (Lowest)'}
|
||||
|
||||
@@ -175,7 +206,8 @@ def compile_patches(xml, names, app_version, segments):
|
||||
offset=int(line.get('Address'),0)-EBOOT_BASE
|
||||
data=encode(line)
|
||||
if not any(start<=offset and offset+len(data)<=end for start,end in segments):
|
||||
raise ValueError(f'{name}: address {line.get("Address")} is outside the eboot')
|
||||
print(f'Warning: {name}: address {line.get("Address")} is outside the eboot; skipping', file=sys.stderr)
|
||||
continue
|
||||
writes.append((offset,data))
|
||||
return writes
|
||||
|
||||
@@ -265,8 +297,17 @@ def main():
|
||||
size=render_size(settings)
|
||||
if size: print(f'{size[0]}x{size[1]}')
|
||||
return
|
||||
names=FPS_PRESETS[a.fps]+[n.strip() for n in a.extra.split(';') if n.strip()]
|
||||
names+=[n for n in effect_patches(read_settings(a.settings)) if n not in names]
|
||||
fps_patches = FPS_PRESETS[a.fps]
|
||||
if os.environ.get('BB_FPS_PATCH') in ('0', 'off', 'false'):
|
||||
fps_patches = []
|
||||
raw_names = fps_patches + [n.strip() for n in a.extra.split(';') if n.strip()]
|
||||
raw_names += [n for n in effect_patches(read_settings(a.settings)) if n not in raw_names]
|
||||
if intel_tonemap_fix() and INTEL_TONEMAP not in raw_names:
|
||||
raw_names.append(INTEL_TONEMAP)
|
||||
names = []
|
||||
for n in raw_names:
|
||||
if n not in names:
|
||||
names.append(n)
|
||||
validate_patch_requirements(names,a.game_dir)
|
||||
segments=eboot_segments((a.out/'eboot.elf').read_bytes())
|
||||
writes=compile_patches(a.xml,names,a.app_version,segments)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
out = Path("out")
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
search_dirs = [
|
||||
Path(os.environ.get("MINGW_BIN", r"C:\msys64\mingw64\bin")),
|
||||
Path(r"C:\msys64\mingw64\bin"),
|
||||
]
|
||||
if os.environ.get("SDL3_DIR"):
|
||||
search_dirs.append(Path(os.environ["SDL3_DIR"]) / "bin")
|
||||
|
||||
for p in os.environ.get("PATH", "").split(os.pathsep):
|
||||
if p.strip():
|
||||
search_dirs.append(Path(p.strip()))
|
||||
|
||||
dlls = [
|
||||
'SDL3.dll',
|
||||
'libxxhash.dll','libzstd.dll','zlib1.dll','liblzma-5.dll','libbz2-1.dll',
|
||||
'libiconv-2.dll','libintl-8.dll','libbrotlicommon.dll','libbrotlidec.dll',
|
||||
'libbrotlienc.dll','libglib-2.0-0.dll','libgmodule-2.0-0.dll','libgobject-2.0-0.dll',
|
||||
'libgio-2.0-0.dll','libpcre2-8-0.dll','libffi-8.dll','libwinpthread-1.dll',
|
||||
'libgcc_s_seh-1.dll','libstdc++-6.dll','libspeex-1.dll','libtheoradec-1.dll',
|
||||
'libtheoraenc-1.dll','libvorbis-0.dll','libvorbisenc-2.dll','libvorbisfile-3.dll',
|
||||
'libogg-0.dll','libopus-0.dll','libvpx-1.dll','libdav1d-7.dll','libva.dll',
|
||||
'libmfx-1.dll','libva_win32.dll','libgsm.dll','libmp3lame-0.dll',
|
||||
'libopencore-amrnb-0.dll','libopencore-amrwb-0.dll','libopenjp2-7.dll','librav1e.dll',
|
||||
'libsharpyuv-0.dll','libwebp-7.dll','libwebpmux-3.dll','libsnappy.dll','libsoxr.dll',
|
||||
'libtwolame-0.dll','libx264-166.dll','libx265.dll','libxvidcore-4.dll',
|
||||
'libxml2-16.dll','libbluray-2.dll','libgmp-10.dll','libgnutls-30.dll',
|
||||
'libhogweed-6.dll','libnettle-8.dll','libp11-kit-0.dll','libtasn1-6.dll',
|
||||
'libidn2-0.dll','libunistring-5.dll','libfontconfig-1.dll','libfreetype-6.dll',
|
||||
'libfribidi-0.dll','libharfbuzz-0.dll','libgraphite2.dll','libthai-0.dll',
|
||||
'libdatrie-1.dll','libpango-1.0-0.dll','libpangocairo-1.0-0.dll','libpangoft2-1.0-0.dll',
|
||||
'libpangowin32-1.0-0.dll','libcairo-2.dll','libcairo-gobject-2.dll','libpixman-1-0.dll',
|
||||
'libpng16-16.dll','libdeflate.dll','libjpeg-8.dll','libjbig-0.dll','libLerc.dll',
|
||||
'libtiff-6.dll','libexpat-1.dll','liblcms2-2.dll','avcodec-62.dll','avformat-62.dll',
|
||||
'avutil-60.dll','swresample-6.dll','swscale-9.dll','libZydis.dll','libZycore.dll'
|
||||
]
|
||||
|
||||
staged = 0
|
||||
for d in dlls:
|
||||
dst = out / d
|
||||
if dst.exists():
|
||||
continue
|
||||
for sdir in search_dirs:
|
||||
src = sdir / d
|
||||
if src.is_file():
|
||||
try:
|
||||
shutil.copy2(src, dst)
|
||||
staged += 1
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
print(f"Runtime DLL staging complete ({staged} new DLLs staged).")
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/* The runtime's own locks, condition variables, threads and clocks.
|
||||
* Linux: pthreads and clock_gettime.
|
||||
* Windows: native SRWLOCK, CONDITION_VARIABLE, CRT threads, QueryPerformanceCounter. */
|
||||
#ifndef BB_HOST_SYNC_H
|
||||
#define BB_HOST_SYNC_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
typedef SRWLOCK HostMutex;
|
||||
#define HOST_MUTEX_INIT SRWLOCK_INIT
|
||||
static inline void host_mutex_init(HostMutex *m) { InitializeSRWLock(m); }
|
||||
static inline void host_lock(HostMutex *m) { AcquireSRWLockExclusive(m); }
|
||||
static inline int host_trylock(HostMutex *m) { return TryAcquireSRWLockExclusive(m) != 0; }
|
||||
static inline void host_unlock(HostMutex *m) { ReleaseSRWLockExclusive(m); }
|
||||
static inline void host_mutex_lock(HostMutex *m) { host_lock(m); }
|
||||
static inline void host_mutex_unlock(HostMutex *m) { host_unlock(m); }
|
||||
|
||||
/* SRW locks are not recursive: the owner and depth make this one so. */
|
||||
typedef struct { SRWLOCK lock; DWORD owner; unsigned depth; } HostRecursiveMutex;
|
||||
#define HOST_RECURSIVE_MUTEX_INIT {SRWLOCK_INIT, 0, 0}
|
||||
static inline void host_recursive_lock(HostRecursiveMutex *m) {
|
||||
DWORD self = GetCurrentThreadId();
|
||||
if (__atomic_load_n(&m->owner, __ATOMIC_RELAXED) == self) { ++m->depth; return; }
|
||||
AcquireSRWLockExclusive(&m->lock);
|
||||
__atomic_store_n(&m->owner, self, __ATOMIC_RELAXED);
|
||||
m->depth = 1;
|
||||
}
|
||||
static inline void host_recursive_unlock(HostRecursiveMutex *m) {
|
||||
if (--m->depth) return;
|
||||
__atomic_store_n(&m->owner, 0, __ATOMIC_RELAXED);
|
||||
ReleaseSRWLockExclusive(&m->lock);
|
||||
}
|
||||
|
||||
typedef SRWLOCK HostRwlock;
|
||||
#define HOST_RWLOCK_INIT SRWLOCK_INIT
|
||||
static inline void host_rwlock_init(HostRwlock *l) { InitializeSRWLock(l); }
|
||||
static inline void host_read_lock(HostRwlock *l) { AcquireSRWLockShared(l); }
|
||||
static inline int host_try_read_lock(HostRwlock *l) { return TryAcquireSRWLockShared(l) != 0; }
|
||||
static inline void host_read_unlock(HostRwlock *l) { ReleaseSRWLockShared(l); }
|
||||
static inline void host_write_lock(HostRwlock *l) { AcquireSRWLockExclusive(l); }
|
||||
static inline int host_try_write_lock(HostRwlock *l) { return TryAcquireSRWLockExclusive(l) != 0; }
|
||||
static inline void host_write_unlock(HostRwlock *l) { ReleaseSRWLockExclusive(l); }
|
||||
|
||||
typedef CONDITION_VARIABLE HostCond;
|
||||
#define HOST_COND_INIT CONDITION_VARIABLE_INIT
|
||||
static inline void host_cond_init(HostCond *c) { InitializeConditionVariable(c); }
|
||||
static inline void host_cond_wait(HostCond *c, HostMutex *m) { SleepConditionVariableSRW(c, m, INFINITE, 0); }
|
||||
/* 0 when woken, ETIMEDOUT once at least ns passed. */
|
||||
static inline int host_cond_timedwait(HostCond *c, HostMutex *m, uint64_t ns) {
|
||||
uint64_t ms = (ns + 999999) / 1000000;
|
||||
if (SleepConditionVariableSRW(c, m, ms >= INFINITE ? INFINITE - 1 : (DWORD)ms, 0)) return 0;
|
||||
return GetLastError() == ERROR_TIMEOUT ? ETIMEDOUT : EINVAL;
|
||||
}
|
||||
static inline void host_cond_signal(HostCond *c) { WakeConditionVariable(c); }
|
||||
static inline void host_cond_broadcast(HostCond *c) { WakeAllConditionVariable(c); }
|
||||
|
||||
typedef uint64_t HostThreadId;
|
||||
static inline HostThreadId host_thread_id(void) { return GetCurrentThreadId(); }
|
||||
static inline int host_thread_equal(HostThreadId a, HostThreadId b) { return a == b; }
|
||||
static inline void host_yield(void) { if (!SwitchToThread()) Sleep(0); }
|
||||
typedef HANDLE HostThread;
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
|
||||
typedef pthread_mutex_t HostMutex;
|
||||
#define HOST_MUTEX_INIT PTHREAD_MUTEX_INITIALIZER
|
||||
static inline void host_mutex_init(HostMutex *m) { pthread_mutex_init(m, NULL); }
|
||||
static inline void host_lock(HostMutex *m) { pthread_mutex_lock(m); }
|
||||
static inline int host_trylock(HostMutex *m) { return pthread_mutex_trylock(m) == 0; }
|
||||
static inline void host_unlock(HostMutex *m) { pthread_mutex_unlock(m); }
|
||||
static inline void host_mutex_lock(HostMutex *m) { host_lock(m); }
|
||||
static inline void host_mutex_unlock(HostMutex *m) { host_unlock(m); }
|
||||
|
||||
typedef pthread_mutex_t HostRecursiveMutex;
|
||||
#define HOST_RECURSIVE_MUTEX_INIT PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
|
||||
static inline void host_recursive_lock(HostRecursiveMutex *m) { pthread_mutex_lock(m); }
|
||||
static inline void host_recursive_unlock(HostRecursiveMutex *m) { pthread_mutex_unlock(m); }
|
||||
|
||||
typedef pthread_rwlock_t HostRwlock;
|
||||
#define HOST_RWLOCK_INIT PTHREAD_RWLOCK_INITIALIZER
|
||||
static inline void host_rwlock_init(HostRwlock *l) { pthread_rwlock_init(l, NULL); }
|
||||
static inline void host_read_lock(HostRwlock *l) { pthread_rwlock_rdlock(l); }
|
||||
static inline int host_try_read_lock(HostRwlock *l) { return pthread_rwlock_tryrdlock(l) == 0; }
|
||||
static inline void host_read_unlock(HostRwlock *l) { pthread_rwlock_unlock(l); }
|
||||
static inline void host_write_lock(HostRwlock *l) { pthread_rwlock_wrlock(l); }
|
||||
static inline int host_try_write_lock(HostRwlock *l) { return pthread_rwlock_trywrlock(l) == 0; }
|
||||
static inline void host_write_unlock(HostRwlock *l) { pthread_rwlock_unlock(l); }
|
||||
|
||||
typedef pthread_cond_t HostCond;
|
||||
#define HOST_COND_INIT PTHREAD_COND_INITIALIZER
|
||||
static inline void host_cond_init(HostCond *c) { pthread_cond_init(c, NULL); }
|
||||
static inline void host_cond_wait(HostCond *c, HostMutex *m) { pthread_cond_wait(c, m); }
|
||||
static inline void host_cond_signal(HostCond *c) { pthread_cond_signal(c); }
|
||||
static inline void host_cond_broadcast(HostCond *c) { pthread_cond_broadcast(c); }
|
||||
|
||||
typedef pthread_t HostThreadId;
|
||||
static inline HostThreadId host_thread_id(void) { return pthread_self(); }
|
||||
static inline int host_thread_equal(HostThreadId a, HostThreadId b) { return pthread_equal(a, b); }
|
||||
static inline void host_yield(void) { sched_yield(); }
|
||||
typedef pthread_t HostThread;
|
||||
#endif
|
||||
|
||||
/* Runtime helper threads. */
|
||||
int host_thread_start(HostThread *thread, size_t stack, void *(*entry)(void *), void *argument);
|
||||
int host_thread_join(HostThread thread);
|
||||
void host_thread_detach(HostThread thread);
|
||||
|
||||
/* Clocks: monotonic and realtime, in nanoseconds. */
|
||||
uint64_t host_monotonic_ns(void);
|
||||
uint64_t host_realtime_ns(void);
|
||||
void host_sleep_ns(uint64_t ns);
|
||||
void host_sleep_until_ns(uint64_t monotonic_deadline);
|
||||
#endif
|
||||
+1125
-51
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -44,7 +44,7 @@ static ABI void *guest_tls_get_addr(const uint64_t *index) {
|
||||
}
|
||||
unsigned char **block=&tls_blocks[index[0]];
|
||||
if (!*block) {
|
||||
*block=calloc(1,tls_modules[index[0]].memsz);
|
||||
*block=runtime_low_calloc(1,tls_modules[index[0]].memsz);
|
||||
if (!*block) { fputs("Cannot allocate module TLS\n",stderr); exit(1); }
|
||||
memcpy(*block,tls_modules[index[0]].data,tls_modules[index[0]].filesz);
|
||||
}
|
||||
|
||||
+88
-3
@@ -2,27 +2,56 @@
|
||||
#define BB_RUNTIME_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#ifndef _WIN32
|
||||
#ifdef _WIN32
|
||||
#include "win32_compat.h"
|
||||
#endif
|
||||
#include "host_sync.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
/* runtime_setjmp/runtime_longjmp (runtime_host.c): every Win64 callee-saved register, and no
|
||||
* SEH unwinding, which cannot pass the guest frames in between (they have no unwind data). */
|
||||
typedef struct { _Alignas(16) unsigned char registers[256]; } RuntimeRecoverBuf;
|
||||
int runtime_setjmp(RuntimeRecoverBuf *buf) __attribute__((returns_twice));
|
||||
__attribute__((noreturn)) void runtime_longjmp(RuntimeRecoverBuf *buf);
|
||||
#define RUNTIME_RECOVER_SET(buf) runtime_setjmp(&(buf))
|
||||
#define RUNTIME_RECOVER_JUMP(buf) runtime_longjmp(&(buf))
|
||||
#else
|
||||
#include <setjmp.h>
|
||||
typedef sigjmp_buf RuntimeRecoverBuf;
|
||||
#define RUNTIME_RECOVER_SET(buf) sigsetjmp(buf, 0)
|
||||
#define RUNTIME_RECOVER_JUMP(buf) siglongjmp(buf, 1)
|
||||
#endif
|
||||
/* Recovery point for speculative guest memory reads on this thread (probe.c fault handler). */
|
||||
extern __thread sigjmp_buf *runtime_fault_recover;
|
||||
extern __thread RuntimeRecoverBuf *runtime_fault_recover;
|
||||
|
||||
/* Restarts the game (in-game settings menu, render resolution change). */
|
||||
void runtime_restart(void);
|
||||
#endif
|
||||
#define ABI __attribute__((sysv_abi))
|
||||
typedef void (ABI *GuestCallback)(void);
|
||||
|
||||
#define CHECK_LOW_ADDR(ptr) do { \
|
||||
uintptr_t _a = (uintptr_t)(ptr); \
|
||||
if (_a && _a >= (1ULL << 40)) { \
|
||||
fprintf(stderr, "AUDIT ERROR: address %p exceeds 1<<40 at %s:%d\n", (void *)_a, __FILE__, __LINE__); \
|
||||
fflush(stderr); \
|
||||
} \
|
||||
} while (0)
|
||||
void runtime_start(uint64_t capabilities);
|
||||
uintptr_t runtime_resolve(const char *name, int is_data);
|
||||
void runtime_report(void);
|
||||
void runtime_finalize(void *dso);
|
||||
uintptr_t runtime_mutex_resolve(const char *name);
|
||||
void runtime_mutex_report(void);
|
||||
void runtime_mutex_dump_info(void *f, void *handle);
|
||||
uintptr_t runtime_memory_resolve(const char *name);
|
||||
void runtime_memory_report(void);
|
||||
int runtime_memory_is_mapped(uintptr_t address, uint64_t size);
|
||||
int runtime_memory_vma_info(uintptr_t address, int *prot, int *type, uintptr_t *end);
|
||||
void runtime_memory_dump_recent_ops(void *file_handle);
|
||||
const char *runtime_import_name(const char *name);
|
||||
uintptr_t runtime_rwlock_resolve(const char *name);
|
||||
void runtime_rwlock_report(void);
|
||||
void runtime_rwlock_dump_info(void *f, void *handle);
|
||||
void runtime_set_libc_tls(const void *data, uint64_t filesz, uint64_t memsz);
|
||||
void runtime_set_module_tls(uint64_t module, const void *data, uint64_t filesz, uint64_t memsz);
|
||||
void runtime_set_procparam(void *param);
|
||||
@@ -31,6 +60,8 @@ uintptr_t runtime_thread_resolve(const char *name);
|
||||
void runtime_thread_report(void);
|
||||
void runtime_set_main_tls(const void *data, uint64_t filesz, uint64_t memsz, uint64_t align);
|
||||
void runtime_thread_attach_main(void);
|
||||
void *runtime_thread_get_tcb(void);
|
||||
ABI void restore_guest_fs(void);
|
||||
int32_t *runtime_errno(void);
|
||||
uintptr_t runtime_sema_resolve(const char *name);
|
||||
void runtime_sema_report(void);
|
||||
@@ -63,7 +94,14 @@ int64_t runtime_file_getdents(int fd, char *buffer, uint64_t size, int64_t *base
|
||||
void runtime_thread_keys_cleanup(void);
|
||||
/* Guest-visible errno values are FreeBSD's. */
|
||||
int32_t runtime_guest_errno(int host_errno);
|
||||
int runtime_memory_reserve_space(void);
|
||||
void *runtime_low_map(size_t size, int prot);
|
||||
void *runtime_low_alloc(size_t size);
|
||||
void *runtime_low_malloc(size_t size);
|
||||
void *runtime_low_calloc(size_t nmemb, size_t size);
|
||||
void *runtime_low_alloc_aligned(size_t size, size_t align);
|
||||
void runtime_low_free(void *ptr);
|
||||
void runtime_low_free_aligned(void *ptr);
|
||||
uintptr_t runtime_ajm_resolve(const char *name);
|
||||
void runtime_ajm_report(void);
|
||||
uintptr_t runtime_audio_resolve(const char *name);
|
||||
@@ -76,4 +114,51 @@ void runtime_savedata_configure(const char *title);
|
||||
uintptr_t runtime_savedata_resolve(const char *name);
|
||||
void runtime_savedata_report(void);
|
||||
void runtime_thread_attach_host(const char *name);
|
||||
|
||||
#include <pthread.h>
|
||||
#include <setjmp.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
typedef void *(ABI *GuestEntry)(void *);
|
||||
typedef struct ThreadAttr {
|
||||
uint32_t magic;
|
||||
int detached, policy, prio, inherit;
|
||||
uint64_t stack, guard, affinity;
|
||||
struct ThreadAttr *next;
|
||||
} ThreadAttr;
|
||||
typedef struct GuestThread {
|
||||
uint64_t *tcb;
|
||||
unsigned char *tls_block;
|
||||
HostThread host;
|
||||
GuestEntry entry;
|
||||
void *argument, *result;
|
||||
ThreadAttr attr;
|
||||
char name[32];
|
||||
int detached, finished, joined, host_owned;
|
||||
#ifdef _WIN32
|
||||
RuntimeRecoverBuf exit_jump;
|
||||
HANDLE win32_handle;
|
||||
DWORD win32_tid;
|
||||
#else
|
||||
jmp_buf exit_jump;
|
||||
#endif
|
||||
const char *blocked_on;
|
||||
uint64_t blocked_resource;
|
||||
uint64_t blocked_tick;
|
||||
uint32_t recent_imports[32];
|
||||
uint32_t recent_import_count;
|
||||
struct GuestThread *next;
|
||||
} GuestThread;
|
||||
|
||||
GuestThread *runtime_thread_get_all(void);
|
||||
GuestThread *runtime_thread_current(void);
|
||||
void *runtime_thread_get_lock(void);
|
||||
void runtime_thread_set_blocked(const char *what, uint64_t resource);
|
||||
void runtime_thread_clear_blocked(void);
|
||||
void runtime_notify_progress(void);
|
||||
uint64_t runtime_audio_get_underruns(void);
|
||||
#endif
|
||||
|
||||
|
||||
+28
-26
@@ -69,7 +69,7 @@ typedef struct {
|
||||
typedef struct { int used, registered[24]; Instance instances[MAX_INSTANCES+1]; } Context;
|
||||
typedef struct { int used, context, canceled; } Batch;
|
||||
|
||||
static pthread_mutex_t lock=PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
|
||||
static HostRecursiveMutex lock = HOST_RECURSIVE_MUTEX_INIT;
|
||||
static Context *contexts[MAX_CONTEXTS+1];
|
||||
static Batch batches[MAX_BATCHES];
|
||||
static size_t jobs_run, frames_decoded, batches_run;
|
||||
@@ -310,11 +310,13 @@ static void run_job(Context *ctx, uint32_t id, Job *job) {
|
||||
if (control && CONTROL_INITIALIZE(f) && control+8<=control_end) { memcpy(in->config,control,4); at9_reset(in); }
|
||||
uint64_t in_size=0;
|
||||
for (int i=0;i<job->input_count;++i) in_size+=job->inputs[i].size;
|
||||
unsigned char scratch[4096];
|
||||
unsigned char *joined=NULL;
|
||||
const unsigned char *input=NULL;
|
||||
if (job->input_count==1) input=job->inputs[0].address;
|
||||
else if (job->input_count>1) {
|
||||
joined=malloc(in_size ? in_size : 1);
|
||||
if (in_size <= sizeof(scratch)) joined=scratch;
|
||||
else joined=malloc(in_size ? in_size : 1);
|
||||
uint64_t at=0;
|
||||
for (int i=0;i<job->input_count;++i) { memcpy(joined+at,job->inputs[i].address,job->inputs[i].size); at+=job->inputs[i].size; }
|
||||
input=joined;
|
||||
@@ -336,7 +338,7 @@ static void run_job(Context *ctx, uint32_t id, Job *job) {
|
||||
if (r) { flags_result|=r; break; }
|
||||
if (!RUN_MULTIPLE_FRAMES(f)) break;
|
||||
}
|
||||
free(joined);
|
||||
if (joined && joined != scratch) free(joined);
|
||||
if (flags_result && ajm_trace())
|
||||
printf("Audio trace: Ajm job instance %u flags %#llx -> result %#x internal %d; input %llu/%llu used, output %llu/%llu written, frames %u\n",
|
||||
id,(unsigned long long)f,flags_result,internal,(unsigned long long)(in_start-in_size),(unsigned long long)in_start,
|
||||
@@ -367,41 +369,41 @@ static void run_job(Context *ctx, uint32_t id, Job *job) {
|
||||
static Context *context(uint32_t id) { return id && id<=MAX_CONTEXTS ? contexts[id] : NULL; }
|
||||
static ABI int32_t ajm_initialize(int64_t reserved, uint32_t *out) {
|
||||
if (!out || reserved) return ERR_INVALID_PARAMETER;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
for (uint32_t i=1;i<=MAX_CONTEXTS;++i) if (!contexts[i]) {
|
||||
contexts[i]=calloc(1,sizeof(Context));
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
if (!contexts[i]) return ERR_OUT_OF_RESOURCES;
|
||||
*out=i; puts("Runtime: Ajm context initialized (ATRAC9 via LibAtrac9)");
|
||||
return 0;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return ERR_OUT_OF_RESOURCES;
|
||||
}
|
||||
static ABI int32_t ajm_finalize(uint32_t id) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
Context *c=context(id);
|
||||
if (!c) { pthread_mutex_unlock(&lock); return ERR_INVALID_CONTEXT; }
|
||||
if (!c) { host_recursive_unlock(&lock); return ERR_INVALID_CONTEXT; }
|
||||
for (int i=0;i<=MAX_INSTANCES;++i) if (c->instances[i].handle) Atrac9ReleaseHandle(c->instances[i].handle);
|
||||
free(c); contexts[id]=NULL;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t ajm_module_register(uint32_t id, uint32_t codec, int64_t reserved) {
|
||||
if (reserved || codec>=24) return ERR_INVALID_PARAMETER;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
Context *c=context(id);
|
||||
int32_t r=!c ? ERR_INVALID_CONTEXT : c->registered[codec] ? ERR_ALREADY_REGISTERED : 0;
|
||||
if (!r) c->registered[codec]=1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
static ABI int32_t ajm_module_unregister(uint32_t id, uint32_t codec) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
Context *c=context(id);
|
||||
int32_t r=!c ? ERR_INVALID_CONTEXT : codec>=24 ? ERR_INVALID_PARAMETER : !c->registered[codec] ? ERR_NOT_REGISTERED : 0;
|
||||
if (!r) c->registered[codec]=0;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
static ABI int32_t ajm_instance_create(uint32_t id, uint32_t codec, uint64_t flags, uint32_t *out) {
|
||||
@@ -409,7 +411,7 @@ static ABI int32_t ajm_instance_create(uint32_t id, uint32_t codec, uint64_t fla
|
||||
if (!(flags & 7)) return ERR_WRONG_REVISION;
|
||||
if (codec!=1) { fprintf(stderr,"STOP: Ajm codec %u (0=MP3, 2=AAC) is not implemented\n",codec); exit(21); }
|
||||
if (ajm_trace()) printf("Audio trace: Ajm instance create codec %u flags %#llx\n",codec,(unsigned long long)flags);
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
Context *c=context(id);
|
||||
int32_t r=!c ? ERR_INVALID_CONTEXT : !c->registered[codec] ? ERR_NOT_REGISTERED : ERR_OUT_OF_RESOURCES;
|
||||
if (c && c->registered[codec]) for (uint32_t i=1;i<=MAX_INSTANCES;++i) if (!c->instances[i].used) {
|
||||
@@ -420,16 +422,16 @@ static ABI int32_t ajm_instance_create(uint32_t id, uint32_t codec, uint64_t fla
|
||||
if (in->format>FORMAT_FLOAT) { in->used=0; r=ERR_INVALID_PARAMETER; break; }
|
||||
*out=i|(codec<<14); r=0; break;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
static ABI int32_t ajm_instance_destroy(uint32_t id, uint32_t instance) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
Context *c=context(id);
|
||||
Instance *in=c ? &c->instances[instance & MAX_INSTANCES] : NULL;
|
||||
int32_t r=!c ? ERR_INVALID_CONTEXT : !(instance & MAX_INSTANCES) || !in->used ? ERR_INVALID_INSTANCE : 0;
|
||||
if (!r) { if (in->handle) Atrac9ReleaseHandle(in->handle); memset(in,0,sizeof(*in)); }
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
static ABI uint32_t ajm_instance_codec(uint32_t instance) { return (instance>>14)&0x1f; }
|
||||
@@ -437,12 +439,12 @@ static ABI int32_t ajm_batch_start(uint32_t id, unsigned char *buffer, uint32_t
|
||||
BatchError *error, uint32_t *out) {
|
||||
(void)priority;
|
||||
if (size & 7) return ERR_MALFORMED_BATCH;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
Context *c=context(id);
|
||||
if (!c) { pthread_mutex_unlock(&lock); return ERR_INVALID_CONTEXT; }
|
||||
if (!c) { host_recursive_unlock(&lock); return ERR_INVALID_CONTEXT; }
|
||||
int slot=-1;
|
||||
for (int i=0;i<MAX_BATCHES;++i) if (!batches[i].used) { slot=i; break; }
|
||||
if (slot<0) { pthread_mutex_unlock(&lock); return ERR_OUT_OF_RESOURCES; }
|
||||
if (slot<0) { host_recursive_unlock(&lock); return ERR_OUT_OF_RESOURCES; }
|
||||
for (unsigned char *p=buffer, *end=buffer+size; p<end;) {
|
||||
Chunk chunk; memcpy(&chunk,p,8);
|
||||
unsigned char *body=p+8;
|
||||
@@ -451,7 +453,7 @@ static ABI int32_t ajm_batch_start(uint32_t id, unsigned char *buffer, uint32_t
|
||||
Job job;
|
||||
if (ident(chunk.word)!=IDENT_JOB || p>end || parse_job(body,chunk.size,&job)) {
|
||||
if (error) { error->error_code=ERR_MALFORMED_BATCH; error->job_address=body-8; error->command_offset=(uint32_t)(body-8-buffer); error->job_return_address=NULL; }
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return ERR_MALFORMED_BATCH;
|
||||
}
|
||||
run_job(c,payload(chunk.word),&job);
|
||||
@@ -459,21 +461,21 @@ static ABI int32_t ajm_batch_start(uint32_t id, unsigned char *buffer, uint32_t
|
||||
batches[slot]=(Batch){1,(int)id,0};
|
||||
++batches_run;
|
||||
*out=(uint32_t)slot+1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t ajm_batch_wait(uint32_t id, uint32_t batch, uint32_t timeout, BatchError *error) {
|
||||
(void)timeout; (void)error;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
int32_t r=!context(id) ? ERR_INVALID_CONTEXT : !batch || batch>MAX_BATCHES || !batches[batch-1].used ? ERR_INVALID_BATCH : 0;
|
||||
if (!r) { if (batches[batch-1].canceled) r=ERR_CANCELLED; batches[batch-1].used=0; }
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
static ABI int32_t ajm_batch_cancel(uint32_t id, uint32_t batch) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_recursive_lock(&lock);
|
||||
int32_t r=!context(id) ? ERR_INVALID_CONTEXT : !batch || batch>MAX_BATCHES || !batches[batch-1].used ? ERR_INVALID_BATCH : 0;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_recursive_unlock(&lock);
|
||||
return r; /* already processed at submission: nothing to cancel */
|
||||
}
|
||||
static ABI int32_t ajm_error_dump(void) { return 0; }
|
||||
|
||||
+124
-59
@@ -11,9 +11,12 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <mmsystem.h>
|
||||
#endif
|
||||
|
||||
#define PORTS 25
|
||||
#define ERR_NOT_OPENED ((int32_t)0x80260001)
|
||||
@@ -36,7 +39,7 @@ typedef struct {
|
||||
uint64_t next_deadline_ns; /* next return of sceAudioOutOutput */
|
||||
int64_t adjust_ns; int window_min, window_count; /* queue level control */
|
||||
uint64_t last_output_us;
|
||||
pthread_mutex_t lock;
|
||||
HostMutex lock;
|
||||
FILE *dump; /* BB_AUDIO_DUMP: raw converted PCM per port */
|
||||
int stats;
|
||||
uint64_t stat_start_ns, stat_last_ns, stat_max_gap_ns; /* BB_AUDIO_STATS */
|
||||
@@ -45,16 +48,23 @@ typedef struct {
|
||||
typedef struct { uint16_t output; uint8_t channel, reserved; int16_t volume; uint16_t reroute; uint64_t flag, reserved64[2]; } PortState;
|
||||
_Static_assert(sizeof(PortState)==32,"AudioOut port state layout");
|
||||
|
||||
static pthread_mutex_t table_lock=PTHREAD_MUTEX_INITIALIZER;
|
||||
static HostMutex table_lock = HOST_MUTEX_INIT;
|
||||
static Port ports[PORTS];
|
||||
static int initialized, sdl_ready=-1;
|
||||
static size_t buffers_out, ports_opened;
|
||||
static uint64_t g_audio_underruns=0;
|
||||
|
||||
static uint64_t now_ns(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return (uint64_t)t.tv_sec*1000000000u+(uint64_t)t.tv_nsec; }
|
||||
static void sleep_until(uint64_t deadline) {
|
||||
struct timespec t={(time_t)(deadline/1000000000u),(long)(deadline%1000000000u)};
|
||||
while (clock_nanosleep(CLOCK_MONOTONIC,TIMER_ABSTIME,&t,NULL)) {}
|
||||
uint64_t runtime_audio_get_underruns(void) { return g_audio_underruns; }
|
||||
|
||||
static uint64_t now_ns(void) { return host_monotonic_ns(); }
|
||||
static void sleep_until(uint64_t deadline) { host_sleep_until_ns(deadline); }
|
||||
|
||||
static inline float soft_clip(float x) {
|
||||
if (x > 1.0f) return 1.0f - 1.0f / (1.0f + (x - 1.0f));
|
||||
if (x < -1.0f) return -1.0f + 1.0f / (1.0f + (-x - 1.0f));
|
||||
return x;
|
||||
}
|
||||
|
||||
static int sdl_audio(void) {
|
||||
if (sdl_ready<0) {
|
||||
const char *mode=getenv("BB_AUDIO");
|
||||
@@ -63,6 +73,7 @@ static int sdl_audio(void) {
|
||||
}
|
||||
return sdl_ready;
|
||||
}
|
||||
|
||||
static int port_range(int type, int *first, int *last) {
|
||||
switch (type) {
|
||||
case 0: *first=0; *last=7; return 1; /* main */
|
||||
@@ -83,12 +94,18 @@ static Port *port_of(int32_t handle, int32_t *error) {
|
||||
}
|
||||
|
||||
static ABI int32_t audio_init(void) {
|
||||
pthread_mutex_lock(&table_lock);
|
||||
host_lock(&table_lock);
|
||||
int32_t r=initialized ? ERR_ALREADY_INIT : 0;
|
||||
if (!initialized) {
|
||||
#ifdef _WIN32
|
||||
timeBeginPeriod(1);
|
||||
#endif
|
||||
}
|
||||
initialized=1;
|
||||
pthread_mutex_unlock(&table_lock);
|
||||
host_unlock(&table_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
static ABI int32_t audio_open(int32_t user, int32_t type, int32_t index, uint32_t length, uint32_t freq, uint32_t param) {
|
||||
(void)user; (void)index;
|
||||
if (!initialized) return ERR_NOT_INIT;
|
||||
@@ -99,21 +116,34 @@ static ABI int32_t audio_open(int32_t user, int32_t type, int32_t index, uint32_
|
||||
int first, last;
|
||||
if (!port_range(type,&first,&last)) return ERR_INVALID_TYPE;
|
||||
static const int channels[8]={1,2,8,1,2,8,8,8};
|
||||
pthread_mutex_lock(&table_lock);
|
||||
host_lock(&table_lock);
|
||||
int id=-1;
|
||||
for (int i=first;i<=last;++i) if (!ports[i].used) { id=i; break; }
|
||||
if (id<0) { pthread_mutex_unlock(&table_lock); return ERR_PORT_FULL; }
|
||||
if (id<0) { host_unlock(&table_lock); return ERR_PORT_FULL; }
|
||||
Port *p=&ports[id];
|
||||
memset(p,0,sizeof(*p));
|
||||
pthread_mutex_init(&p->lock,NULL);
|
||||
host_mutex_init(&p->lock);
|
||||
p->used=1; p->type=type; p->channels=channels[format]; p->is_float=format>=3 && format!=6;
|
||||
p->sample_bytes=p->is_float ? 4 : 2; p->frames=(int)length; p->std_layout=format>=6;
|
||||
for (int c=0;c<8;++c) p->volume[c]=VOLUME_0DB;
|
||||
if (sdl_audio()) {
|
||||
SDL_AudioSpec spec={p->is_float ? SDL_AUDIO_F32 : SDL_AUDIO_S16, p->channels, 48000};
|
||||
p->stream=SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,&spec,NULL,NULL);
|
||||
if (p->stream) SDL_ResumeAudioStreamDevice(p->stream);
|
||||
else fprintf(stderr,"Runtime: SDL audio stream failed (%s); port %d uses the timer sink\n",SDL_GetError(),id);
|
||||
if (p->stream) {
|
||||
SDL_ResumeAudioStreamDevice(p->stream);
|
||||
/* Prefill buffer periods of silence to give initial playback headroom (configurable via BB_AUDIO_PREFILL, default 2) */
|
||||
int prefill_count = 2;
|
||||
const char *env_prefill = getenv("BB_AUDIO_PREFILL");
|
||||
if (env_prefill && *env_prefill) {
|
||||
int pf = atoi(env_prefill);
|
||||
if (pf >= 1 && pf <= 8) prefill_count = pf;
|
||||
}
|
||||
unsigned char silence[2048*8*4]={0};
|
||||
size_t prefill_bytes=(size_t)p->frames*(size_t)p->channels*(size_t)p->sample_bytes*(size_t)prefill_count;
|
||||
if (prefill_bytes<=sizeof(silence)) SDL_PutAudioStreamData(p->stream,silence,(int)prefill_bytes);
|
||||
} else {
|
||||
fprintf(stderr,"Runtime: SDL audio stream failed (%s); port %d uses the timer sink\n",SDL_GetError(),id);
|
||||
}
|
||||
}
|
||||
p->stats=getenv("BB_AUDIO_STATS")!=NULL;
|
||||
const char *dump=getenv("BB_AUDIO_DUMP");
|
||||
@@ -123,47 +153,101 @@ static ABI int32_t audio_open(int32_t user, int32_t type, int32_t index, uint32_
|
||||
p->dump=fopen(path,"wb");
|
||||
}
|
||||
++ports_opened;
|
||||
pthread_mutex_unlock(&table_lock);
|
||||
host_unlock(&table_lock);
|
||||
printf("Runtime: audio port %d opened (type %d, %d ch, %s, %u frames)\n",id,type,p->channels,p->is_float ? "float" : "s16",length);
|
||||
return (type<<16) | id | 0x20000000;
|
||||
}
|
||||
|
||||
static ABI int32_t audio_close(int32_t handle) {
|
||||
pthread_mutex_lock(&table_lock);
|
||||
host_lock(&table_lock);
|
||||
int32_t error=0;
|
||||
Port *p=port_of(handle,&error);
|
||||
if (p) {
|
||||
pthread_mutex_lock(&p->lock);
|
||||
host_lock(&p->lock);
|
||||
if (p->stream) SDL_DestroyAudioStream(p->stream);
|
||||
if (p->dump) fclose(p->dump);
|
||||
p->dump=NULL;
|
||||
p->stream=NULL; p->used=0;
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
host_unlock(&p->lock);
|
||||
}
|
||||
pthread_mutex_unlock(&table_lock);
|
||||
host_unlock(&table_lock);
|
||||
return p ? 0 : error;
|
||||
}
|
||||
|
||||
/* `pace`: wait for this port's next period. sceAudioOutOutputs waits once for all its ports. */
|
||||
static int32_t output_port(int32_t handle, const void *data, int pace) {
|
||||
int32_t error=0;
|
||||
pthread_mutex_lock(&table_lock);
|
||||
host_lock(&table_lock);
|
||||
Port *p=port_of(handle,&error);
|
||||
pthread_mutex_unlock(&table_lock);
|
||||
host_unlock(&table_lock);
|
||||
if (!p) return error;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
#ifdef _WIN32
|
||||
static _Thread_local int s_audio_prio_set=0;
|
||||
if (!s_audio_prio_set) {
|
||||
/* Use THREAD_PRIORITY_HIGHEST: avoids starvation of Haswell 4C/4T CPU while maintaining low latency */
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
s_audio_prio_set=1;
|
||||
}
|
||||
#endif
|
||||
host_lock(&p->lock);
|
||||
size_t samples=(size_t)p->frames*(size_t)p->channels, bytes=samples*(size_t)p->sample_bytes;
|
||||
uint64_t period=(uint64_t)p->frames*1000000000u/48000u;
|
||||
if (data) {
|
||||
/* Apply per-channel volume and map PS4 8ch (L R C LFE SL SR BL BR) to SDL 7.1 order. */
|
||||
/* Apply per-channel volume, channel remap, and soft-knee saturation to prevent harsh clipping */
|
||||
unsigned char converted[2048*8*4];
|
||||
static const int remap[8]={0,1,2,3,6,7,4,5};
|
||||
float master_gain=(p->channels==8) ? 0.60f : 1.0f;
|
||||
for (size_t f=0;f<(size_t)p->frames;++f) for (int c=0;c<p->channels;++c) {
|
||||
int target=p->channels==8 && !p->std_layout ? remap[c] : c;
|
||||
size_t from=f*(size_t)p->channels+(size_t)c, to=f*(size_t)p->channels+(size_t)target;
|
||||
float gain=(float)p->volume[c]/VOLUME_0DB;
|
||||
if (p->is_float) { float v; memcpy(&v,(const char *)data+from*4,4); v*=gain; memcpy(converted+to*4,&v,4); }
|
||||
else { int16_t v; memcpy(&v,(const char *)data+from*2,2); v=(int16_t)((float)v*gain); memcpy(converted+to*2,&v,2); }
|
||||
float gain=((float)p->volume[c]/VOLUME_0DB)*master_gain;
|
||||
if (p->is_float) {
|
||||
float v; memcpy(&v,(const char *)data+from*4,4);
|
||||
v*=gain;
|
||||
v=soft_clip(v);
|
||||
memcpy(converted+to*4,&v,4);
|
||||
} else {
|
||||
int16_t v; memcpy(&v,(const char *)data+from*2,2);
|
||||
float fv=((float)v/32768.0f)*gain;
|
||||
fv=soft_clip(fv);
|
||||
int32_t iv=(int32_t)(fv*32767.0f);
|
||||
if (iv>32767) iv=32767; else if (iv<-32768) iv=-32768;
|
||||
int16_t sv=(int16_t)iv;
|
||||
memcpy(converted+to*2,&sv,2);
|
||||
}
|
||||
}
|
||||
if (p->dump) fwrite(converted,1,bytes,p->dump);
|
||||
|
||||
if (pace) {
|
||||
uint64_t now=now_ns();
|
||||
if (!p->next_deadline_ns || now>p->next_deadline_ns+8*period) p->next_deadline_ns=now; /* start or stall: restart cadence */
|
||||
else sleep_until(p->next_deadline_ns);
|
||||
p->next_deadline_ns+=period;
|
||||
}
|
||||
|
||||
if (p->stream) {
|
||||
/* Steer the SDL queue against hardware device rate */
|
||||
int low=2*(int)bytes;
|
||||
int queued=SDL_GetAudioStreamQueued(p->stream);
|
||||
if (queued<(int)bytes) {
|
||||
if (buffers_out>0) ++g_audio_underruns;
|
||||
static const unsigned char silence[2048*8*4];
|
||||
int fill=low-queued<(int)bytes ? low-queued : (int)bytes;
|
||||
if (fill>0 && fill<=(int)sizeof(silence)) {
|
||||
SDL_PutAudioStreamData(p->stream,silence,fill);
|
||||
}
|
||||
queued=low;
|
||||
}
|
||||
if (!p->window_count || queued<p->window_min) p->window_min=queued;
|
||||
if (++p->window_count>=32) {
|
||||
p->adjust_ns=p->window_min>low+2*(int)bytes ? (int64_t)(period/32) :
|
||||
p->window_min<low ? -(int64_t)(period/32) : 0;
|
||||
p->window_count=0;
|
||||
}
|
||||
p->next_deadline_ns+=(uint64_t)p->adjust_ns;
|
||||
SDL_PutAudioStreamData(p->stream,converted,(int)bytes);
|
||||
}
|
||||
|
||||
if (p->stats) {
|
||||
uint64_t now=now_ns();
|
||||
if (!p->stat_start_ns) { p->stat_start_ns=now; p->stat_min_queued=1<<30; }
|
||||
@@ -181,35 +265,13 @@ static int32_t output_port(int32_t handle, const void *data, int pace) {
|
||||
p->stat_start_ns=now; p->stat_max_gap_ns=0; p->stat_starved=0; p->stat_buffers=0; p->stat_min_queued=1<<30;
|
||||
}
|
||||
}
|
||||
if (pace) {
|
||||
uint64_t now=now_ns();
|
||||
if (!p->next_deadline_ns || now>p->next_deadline_ns+8*period) p->next_deadline_ns=now; /* start or stall: restart cadence */
|
||||
else sleep_until(p->next_deadline_ns);
|
||||
p->next_deadline_ns+=period;
|
||||
}
|
||||
if (p->stream) {
|
||||
/* The device drains the queue in quanta (21 ms on PipeWire), so the level
|
||||
is a sawtooth: its minimum over ~32 buffers is what gets controlled. */
|
||||
int low=2*(int)bytes, queued=SDL_GetAudioStreamQueued(p->stream);
|
||||
if (queued<(int)bytes) {
|
||||
static const unsigned char silence[2048*8*4];
|
||||
SDL_PutAudioStreamData(p->stream,silence,low-queued<(int)bytes ? low-queued : (int)bytes);
|
||||
queued=low;
|
||||
}
|
||||
if (!p->window_count || queued<p->window_min) p->window_min=queued;
|
||||
if (++p->window_count==32) {
|
||||
p->adjust_ns=p->window_min>low+2*(int)bytes ? (int64_t)(period/32) : p->window_min<low ? -(int64_t)(period/32) : 0;
|
||||
p->window_count=0;
|
||||
}
|
||||
p->next_deadline_ns+=(uint64_t)p->adjust_ns;
|
||||
SDL_PutAudioStreamData(p->stream,converted,(int)bytes);
|
||||
}
|
||||
++buffers_out;
|
||||
}
|
||||
p->last_output_us=now_ns()/1000;
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
host_unlock(&p->lock);
|
||||
return data ? (int32_t)samples : 0;
|
||||
}
|
||||
|
||||
static ABI int32_t audio_output(int32_t handle, const void *data) { return output_port(handle,data,1); }
|
||||
typedef struct { int32_t handle; const void *data; } OutputParam;
|
||||
static ABI int32_t audio_outputs(const OutputParam *params, uint32_t count) {
|
||||
@@ -221,26 +283,28 @@ static ABI int32_t audio_outputs(const OutputParam *params, uint32_t count) {
|
||||
for (uint32_t i=0;i<count;++i) { int32_t r=output_port(params[i].handle,params[i].data,i==0); if (r<0) return r; result=r; }
|
||||
return result;
|
||||
}
|
||||
|
||||
static ABI int32_t audio_volume(int32_t handle, int32_t flags, const int32_t *volume) {
|
||||
int32_t error=0;
|
||||
pthread_mutex_lock(&table_lock);
|
||||
host_lock(&table_lock);
|
||||
Port *p=port_of(handle,&error);
|
||||
pthread_mutex_unlock(&table_lock);
|
||||
host_unlock(&table_lock);
|
||||
if (!p) return error;
|
||||
if (!volume) return ERR_INVALID_POINTER;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
host_lock(&p->lock);
|
||||
for (int c=0;c<8;++c) if (flags & (1<<c)) {
|
||||
if (volume[c]<0 || volume[c]>VOLUME_0DB) { pthread_mutex_unlock(&p->lock); return ERR_INVALID_VOLUME; }
|
||||
if (volume[c]<0 || volume[c]>VOLUME_0DB) { host_unlock(&p->lock); return ERR_INVALID_VOLUME; }
|
||||
p->volume[c]=volume[c];
|
||||
}
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
host_unlock(&p->lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t audio_state(int32_t handle, PortState *state) {
|
||||
int32_t error=0;
|
||||
pthread_mutex_lock(&table_lock);
|
||||
host_lock(&table_lock);
|
||||
Port *p=port_of(handle,&error);
|
||||
pthread_mutex_unlock(&table_lock);
|
||||
host_unlock(&table_lock);
|
||||
if (!p) return error;
|
||||
if (!state) return ERR_INVALID_POINTER;
|
||||
memset(state,0,sizeof(*state));
|
||||
@@ -251,11 +315,12 @@ static ABI int32_t audio_state(int32_t handle, PortState *state) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t audio_last_time(int32_t handle, uint64_t *time) {
|
||||
int32_t error=0;
|
||||
pthread_mutex_lock(&table_lock);
|
||||
host_lock(&table_lock);
|
||||
Port *p=port_of(handle,&error);
|
||||
pthread_mutex_unlock(&table_lock);
|
||||
host_unlock(&table_lock);
|
||||
if (!p) return error;
|
||||
if (!time) return ERR_INVALID_POINTER;
|
||||
*time=p->last_output_us; return 0;
|
||||
|
||||
+220
-17
@@ -8,7 +8,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#ifdef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -16,6 +16,85 @@
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <direct.h>
|
||||
#include <inttypes.h>
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC 0
|
||||
#endif
|
||||
#ifndef O_DIRECTORY
|
||||
#define O_DIRECTORY 0
|
||||
#endif
|
||||
#ifndef O_NONBLOCK
|
||||
#define O_NONBLOCK 0
|
||||
#endif
|
||||
#ifndef O_SYNC
|
||||
#define O_SYNC 0
|
||||
#endif
|
||||
#ifndef mkdir
|
||||
#define mkdir(p, m) _mkdir(p)
|
||||
#endif
|
||||
#define fsync(fd) _commit(fd)
|
||||
#ifndef realpath
|
||||
#define realpath(N, R) _fullpath(R, N, MAX_PATH)
|
||||
#endif
|
||||
static ssize_t win_pread(int fd, void *buf, size_t count, int64_t offset) {
|
||||
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
DWORD err = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: win_pread _get_osfhandle(fd=%d) failed: Win32 error %lu\n", fd, (unsigned long)err);
|
||||
return -1;
|
||||
}
|
||||
OVERLAPPED ov = {0};
|
||||
ov.Offset = (DWORD)(offset & 0xFFFFFFFF);
|
||||
ov.OffsetHigh = (DWORD)((offset >> 32) & 0xFFFFFFFF);
|
||||
DWORD read_bytes = 0;
|
||||
DWORD to_read = (DWORD)(count > 0xFFFFFFFFULL ? 0xFFFFFFFFULL : count);
|
||||
if (!ReadFile(h, buf, to_read, &read_bytes, &ov)) {
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_HANDLE_EOF) return 0;
|
||||
fprintf(stderr, "Runtime ERROR: ReadFile(fd=%d, offset=%" PRId64 ", count=%zu) failed: Win32 error %lu\n",
|
||||
fd, offset, count, (unsigned long)err);
|
||||
return -1;
|
||||
}
|
||||
return (ssize_t)read_bytes;
|
||||
}
|
||||
static ssize_t win_pwrite(int fd, const void *buf, size_t count, int64_t offset) {
|
||||
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
DWORD err = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: win_pwrite _get_osfhandle(fd=%d) failed: Win32 error %lu\n", fd, (unsigned long)err);
|
||||
return -1;
|
||||
}
|
||||
OVERLAPPED ov = {0};
|
||||
ov.Offset = (DWORD)(offset & 0xFFFFFFFF);
|
||||
ov.OffsetHigh = (DWORD)((offset >> 32) & 0xFFFFFFFF);
|
||||
DWORD written_bytes = 0;
|
||||
DWORD to_write = (DWORD)(count > 0xFFFFFFFFULL ? 0xFFFFFFFFULL : count);
|
||||
if (!WriteFile(h, buf, to_write, &written_bytes, &ov)) {
|
||||
DWORD err = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: WriteFile(fd=%d, offset=%" PRId64 ", count=%zu) failed: Win32 error %lu\n",
|
||||
fd, offset, count, (unsigned long)err);
|
||||
return -1;
|
||||
}
|
||||
return (ssize_t)written_bytes;
|
||||
}
|
||||
#ifndef pread
|
||||
#define pread win_pread
|
||||
#endif
|
||||
#ifndef pwrite
|
||||
#define pwrite win_pwrite
|
||||
#endif
|
||||
#else
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
#define ERR(n) ((int32_t)(UINT32_C(0x80020000)|(n)))
|
||||
#define MAX_FILES 1024
|
||||
#define MAX_MOUNTS 16
|
||||
@@ -109,7 +188,11 @@ static int host_flags(int flags) {
|
||||
if (flags&0x400) r|=O_TRUNC;
|
||||
if (flags&0x800) r|=O_EXCL;
|
||||
if (flags&0x20000) r|=O_DIRECTORY;
|
||||
#ifdef _WIN32
|
||||
return r|O_CLOEXEC|O_BINARY;
|
||||
#else
|
||||
return r|O_CLOEXEC;
|
||||
#endif
|
||||
}
|
||||
static void free_listing(Listing *l) { if (l) { free(l->names); free(l->offsets); free(l->types); free(l); } }
|
||||
static Listing *list_directory(const char *path) {
|
||||
@@ -129,6 +212,16 @@ static Listing *list_directory(const char *path) {
|
||||
if (!l->offsets || !l->types || !l->names) { fputs("Out of memory listing directory\n",stderr); exit(1); }
|
||||
memcpy(l->names+bytes,e->d_name,n);
|
||||
l->offsets[l->count]=bytes;
|
||||
#ifdef _WIN32
|
||||
unsigned char type=0;
|
||||
char full[2048];
|
||||
snprintf(full,sizeof(full),"%s/%s",path,e->d_name);
|
||||
struct _stat64 entry;
|
||||
if (!_stat64(full,&entry)) {
|
||||
type=S_ISDIR(entry.st_mode) ? 4 : S_ISREG(entry.st_mode) ? 8 : 0;
|
||||
}
|
||||
l->types[l->count]=type;
|
||||
#else
|
||||
unsigned char type=e->d_type;
|
||||
if (type==DT_LNK || type==DT_UNKNOWN) {
|
||||
struct stat entry;
|
||||
@@ -136,11 +229,24 @@ static Listing *list_directory(const char *path) {
|
||||
type=S_ISDIR(entry.st_mode) ? DT_DIR : S_ISREG(entry.st_mode) ? DT_REG : type;
|
||||
}
|
||||
l->types[l->count]=type==DT_DIR ? 4 : type==DT_REG ? 8 : type==DT_LNK ? 10 : 0;
|
||||
#endif
|
||||
++l->count; bytes+=n;
|
||||
}
|
||||
closedir(d);
|
||||
return l;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
static void convert_stat(const struct _stat64 *s,GuestStat *g) {
|
||||
memset(g,0,sizeof(*g));
|
||||
g->dev=(uint32_t)s->st_dev; g->ino=(uint32_t)s->st_ino;
|
||||
g->mode=(uint16_t)s->st_mode; g->nlink=(uint16_t)s->st_nlink;
|
||||
g->size=s->st_size; g->blocks=(s->st_size+511)/512; g->blksize=4096;
|
||||
g->atime=(GuestTimespec){s->st_atime,0};
|
||||
g->mtime=(GuestTimespec){s->st_mtime,0};
|
||||
g->ctime=(GuestTimespec){s->st_ctime,0};
|
||||
g->birthtime=g->ctime;
|
||||
}
|
||||
#else
|
||||
static void convert_stat(const struct stat *s,GuestStat *g) {
|
||||
memset(g,0,sizeof(*g));
|
||||
g->dev=(uint32_t)s->st_dev; g->ino=(uint32_t)s->st_ino;
|
||||
@@ -151,6 +257,7 @@ static void convert_stat(const struct stat *s,GuestStat *g) {
|
||||
g->ctime=(GuestTimespec){s->st_ctim.tv_sec,s->st_ctim.tv_nsec};
|
||||
g->birthtime=g->ctime;
|
||||
}
|
||||
#endif
|
||||
static File *get(int fd) {
|
||||
if (fd<3 || fd>=MAX_FILES || !files[fd].used) return NULL;
|
||||
return &files[fd];
|
||||
@@ -169,16 +276,42 @@ static int64_t do_open(const char *guest,int flags,int mode) {
|
||||
if (game_path(guest) && (flags & (3|0x8|0x200|0x400|0x800))) return -EROFS;
|
||||
char path[1024];
|
||||
int e=translate(guest,path,sizeof(path));
|
||||
if (e) return -e;
|
||||
int host=open(path,host_flags(flags),mode ? mode : 0644);
|
||||
if (host<0) {
|
||||
e=errno;
|
||||
if (e==ENOENT) { ++missing; printf("Runtime: open(%s) -> not found\n",guest); }
|
||||
return -e;
|
||||
}
|
||||
struct stat s;
|
||||
int host=-1;
|
||||
Listing *dir=NULL;
|
||||
if (!fstat(host,&s) && S_ISDIR(s.st_mode)) dir=list_directory(path);
|
||||
#ifdef _WIN32
|
||||
struct _stat64 s;
|
||||
if (!_stat64(path,&s) && S_ISDIR(s.st_mode)) {
|
||||
HANDLE h = CreateFileA(path, FILE_LIST_DIRECTORY, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
|
||||
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
if (h != INVALID_HANDLE_VALUE) {
|
||||
host = _open_osfhandle((intptr_t)h, _O_RDONLY);
|
||||
} else {
|
||||
DWORD err = GetLastError();
|
||||
fprintf(stderr, "Runtime WARNING: CreateFileA(dir '%s') failed: Win32 error %lu\n", path, (unsigned long)err);
|
||||
}
|
||||
if (host >= 0) dir = list_directory(path);
|
||||
}
|
||||
#else
|
||||
struct stat s;
|
||||
#endif
|
||||
if (host < 0) {
|
||||
host=open(path,host_flags(flags),mode ? mode : 0644);
|
||||
if (host<0) {
|
||||
e=errno;
|
||||
if (e==ENOENT) { ++missing; printf("Runtime: open(%s) -> not found\n",guest); }
|
||||
else {
|
||||
DWORD win_err = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: open('%s' -> '%s', flags=0x%x) failed: errno=%d, Win32 error %lu\n",
|
||||
guest, path, flags, e, (unsigned long)win_err);
|
||||
}
|
||||
return -e;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (!_fstat64(host,&s) && S_ISDIR(s.st_mode)) dir=list_directory(path);
|
||||
#else
|
||||
if (!fstat(host,&s) && S_ISDIR(s.st_mode)) dir=list_directory(path);
|
||||
#endif
|
||||
}
|
||||
pthread_mutex_lock(&lock);
|
||||
int fd=-1;
|
||||
for (int i=3;i<MAX_FILES;++i) if (!files[i].used) { fd=i; break; }
|
||||
@@ -187,6 +320,7 @@ static int64_t do_open(const char *guest,int flags,int mode) {
|
||||
snprintf(files[fd].path,sizeof(files[fd].path),"%s",guest);
|
||||
++opens;
|
||||
pthread_mutex_unlock(&lock);
|
||||
CHECK_LOW_ADDR((uintptr_t)fd);
|
||||
if (audio_trace() && strstr(guest,"sound/")) printf("Audio trace: open(%s) -> fd %d, %lld bytes\n",guest,fd,(long long)s.st_size);
|
||||
const char *mod_trace=getenv("BB_MOD_TRACE"), *mod_root=getenv("BB_MODS_DIR");
|
||||
if (mod_trace && mod_trace[0]=='1' && mod_root) {
|
||||
@@ -231,17 +365,46 @@ static int64_t do_read(int fd,void *buffer,uint64_t size) {
|
||||
int h=host_fd(fd);
|
||||
if (h<0) return -EBADF;
|
||||
touch_for_write(buffer,size);
|
||||
#ifdef _WIN32
|
||||
uint64_t total_read = 0;
|
||||
while (total_read < size) {
|
||||
size_t chunk = (size - total_read > 0x40000000ULL) ? 0x40000000ULL : (size_t)(size - total_read);
|
||||
int n = read(h, (char *)buffer + total_read, (unsigned int)chunk);
|
||||
if (n < 0) {
|
||||
int e = errno;
|
||||
DWORD werr = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: read(fd %d, chunk %zu) failed: errno %d, Win32 error %lu\n",
|
||||
fd, chunk, e, (unsigned long)werr);
|
||||
if (audio_trace()) printf("Audio trace: read(fd %d, %llu) failed, errno %d\n",fd,(unsigned long long)size,e);
|
||||
return -e;
|
||||
}
|
||||
if (n == 0) break;
|
||||
total_read += (uint64_t)n;
|
||||
}
|
||||
__atomic_add_fetch(&reads,1,__ATOMIC_RELAXED); __atomic_add_fetch(&bytes_read,total_read,__ATOMIC_RELAXED);
|
||||
return (int64_t)total_read;
|
||||
#else
|
||||
ssize_t n=read(h,buffer,size);
|
||||
if (n<0) { if (audio_trace()) printf("Audio trace: read(fd %d, %llu) failed, errno %d\n",fd,(unsigned long long)size,errno); return -errno; }
|
||||
__atomic_add_fetch(&reads,1,__ATOMIC_RELAXED); __atomic_add_fetch(&bytes_read,(uint64_t)n,__ATOMIC_RELAXED);
|
||||
return n;
|
||||
#endif
|
||||
}
|
||||
static int64_t do_pread(int fd,void *buffer,uint64_t size,int64_t offset) {
|
||||
int h=host_fd(fd);
|
||||
if (h<0) return -EBADF;
|
||||
touch_for_write(buffer,size);
|
||||
ssize_t n=pread(h,buffer,size,offset);
|
||||
if (n<0) { if (audio_trace()) printf("Audio trace: pread(fd %d, %llu @%lld) failed, errno %d\n",fd,(unsigned long long)size,(long long)offset,errno); return -errno; }
|
||||
if (n<0) {
|
||||
int e = errno;
|
||||
#ifdef _WIN32
|
||||
DWORD werr = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: pread(fd %d, size %llu, offset %lld) failed: errno %d, Win32 error %lu\n",
|
||||
fd, (unsigned long long)size, (long long)offset, e, (unsigned long)werr);
|
||||
#endif
|
||||
if (audio_trace()) printf("Audio trace: pread(fd %d, %llu @%lld) failed, errno %d\n",fd,(unsigned long long)size,(long long)offset,e);
|
||||
return -e;
|
||||
}
|
||||
__atomic_add_fetch(&reads,1,__ATOMIC_RELAXED); __atomic_add_fetch(&bytes_read,(uint64_t)n,__ATOMIC_RELAXED);
|
||||
return n;
|
||||
}
|
||||
@@ -268,24 +431,54 @@ static int64_t do_lseek(int fd,int64_t offset,int whence) {
|
||||
if (whence==0 && offset>=0) { f->position=(size_t)offset; return offset; }
|
||||
return -EINVAL;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
int64_t r = _lseeki64(f->host, offset, whence);
|
||||
if (r < 0) {
|
||||
int e = errno;
|
||||
DWORD werr = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: _lseeki64(fd %d, offset %lld, whence %d) failed: errno %d, Win32 error %lu\n",
|
||||
fd, (long long)offset, whence, e, (unsigned long)werr);
|
||||
return -e;
|
||||
}
|
||||
return r;
|
||||
#else
|
||||
off_t r=lseek(f->host,offset,whence);
|
||||
return r<0 ? -errno : r;
|
||||
#endif
|
||||
}
|
||||
static int64_t do_fstat(int fd,GuestStat *out) {
|
||||
int h=host_fd(fd);
|
||||
if (h<0) return -EBADF;
|
||||
if (!out) return -EFAULT;
|
||||
#ifdef _WIN32
|
||||
struct _stat64 s;
|
||||
if (_fstat64(h,&s)) {
|
||||
int e = errno;
|
||||
DWORD werr = GetLastError();
|
||||
fprintf(stderr, "Runtime ERROR: _fstat64(fd %d) failed: errno %d, Win32 error %lu\n", fd, e, (unsigned long)werr);
|
||||
return -e;
|
||||
}
|
||||
convert_stat(&s,out); return 0;
|
||||
#else
|
||||
struct stat s;
|
||||
if (fstat(h,&s)) return -errno;
|
||||
convert_stat(&s,out); return 0;
|
||||
#endif
|
||||
}
|
||||
static int64_t do_stat(const char *guest,GuestStat *out) {
|
||||
char path[1024]; struct stat s;
|
||||
char path[1024];
|
||||
int e=translate(guest,path,sizeof(path));
|
||||
if (e) return -e;
|
||||
if (!out) return -EFAULT;
|
||||
#ifdef _WIN32
|
||||
struct _stat64 s;
|
||||
if (_stat64(path,&s)) return -errno;
|
||||
convert_stat(&s,out); return 0;
|
||||
#else
|
||||
struct stat s;
|
||||
if (stat(path,&s)) return -errno;
|
||||
convert_stat(&s,out); return 0;
|
||||
#endif
|
||||
}
|
||||
static int64_t do_getdents(int fd,char *buffer,uint64_t size,int64_t *basep) {
|
||||
pthread_mutex_lock(&lock);
|
||||
@@ -317,6 +510,9 @@ static int64_t do_getdents(int fd,char *buffer,uint64_t size,int64_t *basep) {
|
||||
return result;
|
||||
}
|
||||
static int64_t path_op(const char *guest,int op,int mode) {
|
||||
#ifdef _WIN32
|
||||
(void)mode;
|
||||
#endif
|
||||
if (game_path(guest)) return -EROFS;
|
||||
char path[1024];
|
||||
int e=translate(guest,path,sizeof(path));
|
||||
@@ -335,14 +531,26 @@ static int64_t do_rename(const char *from,const char *to) {
|
||||
static int64_t do_ftruncate(int fd,int64_t length) {
|
||||
int h=host_fd(fd);
|
||||
if (h<0) return -EBADF;
|
||||
#ifdef _WIN32
|
||||
return _chsize_s(h,length) ? -errno : 0;
|
||||
#else
|
||||
return ftruncate(h,length) ? -errno : 0;
|
||||
#endif
|
||||
}
|
||||
static int64_t do_truncate(const char *guest,int64_t length) {
|
||||
if (game_path(guest)) return -EROFS;
|
||||
char path[1024];
|
||||
int e=translate(guest,path,sizeof(path));
|
||||
if (e) return -e;
|
||||
#ifdef _WIN32
|
||||
int h = open(path, O_RDWR | O_BINARY);
|
||||
if (h < 0) return -errno;
|
||||
int r = _chsize_s(h, length);
|
||||
close(h);
|
||||
return r ? -errno : 0;
|
||||
#else
|
||||
return truncate(path,length) ? -errno : 0;
|
||||
#endif
|
||||
}
|
||||
static int64_t do_fsync(int fd) { int h=host_fd(fd); if (h<0) return -EBADF; return fsync(h) ? -errno : 0; }
|
||||
static int64_t do_access(const char *guest,int mode) {
|
||||
@@ -419,8 +627,3 @@ void runtime_file_report(void) {
|
||||
printf("Runtime: files opened=%zu, reads=%zu (%llu bytes), writes=%zu, not found=%zu\n",
|
||||
opens,reads,(unsigned long long)bytes_read,writes,missing);
|
||||
}
|
||||
#else
|
||||
uintptr_t runtime_file_resolve(const char *name) { (void)name; return 0; }
|
||||
void runtime_file_report(void) {}
|
||||
void runtime_file_configure(const char *a,const char *u) { (void)a; (void)u; }
|
||||
#endif
|
||||
@@ -0,0 +1,131 @@
|
||||
/* host_sync.h: the runtime's threads, clocks and sleeps on each host. */
|
||||
#define _GNU_SOURCE
|
||||
#include "runtime.h"
|
||||
#include <stdlib.h>
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
|
||||
/* RuntimeRecoverBuf: rbx rbp rdi rsi r12-r15, rsp after the return, return address, xmm6-xmm15,
|
||||
* MXCSR, x87 control word. */
|
||||
__asm__(".text\n"
|
||||
".globl runtime_setjmp\n"
|
||||
".def runtime_setjmp; .scl 2; .type 32; .endef\n"
|
||||
".p2align 4\n"
|
||||
"runtime_setjmp:\n"
|
||||
" movq %rbx, 0(%rcx)\n movq %rbp, 8(%rcx)\n movq %rdi, 16(%rcx)\n movq %rsi, 24(%rcx)\n"
|
||||
" movq %r12, 32(%rcx)\n movq %r13, 40(%rcx)\n movq %r14, 48(%rcx)\n movq %r15, 56(%rcx)\n"
|
||||
" leaq 8(%rsp), %rdx\n movq %rdx, 64(%rcx)\n movq (%rsp), %rdx\n movq %rdx, 72(%rcx)\n"
|
||||
" movups %xmm6, 80(%rcx)\n movups %xmm7, 96(%rcx)\n movups %xmm8, 112(%rcx)\n"
|
||||
" movups %xmm9, 128(%rcx)\n movups %xmm10, 144(%rcx)\n movups %xmm11, 160(%rcx)\n"
|
||||
" movups %xmm12, 176(%rcx)\n movups %xmm13, 192(%rcx)\n movups %xmm14, 208(%rcx)\n"
|
||||
" movups %xmm15, 224(%rcx)\n stmxcsr 240(%rcx)\n fnstcw 244(%rcx)\n"
|
||||
" xorl %eax, %eax\n ret\n"
|
||||
".globl runtime_longjmp\n"
|
||||
".def runtime_longjmp; .scl 2; .type 32; .endef\n"
|
||||
".p2align 4\n"
|
||||
"runtime_longjmp:\n"
|
||||
" movq 0(%rcx), %rbx\n movq 8(%rcx), %rbp\n movq 16(%rcx), %rdi\n movq 24(%rcx), %rsi\n"
|
||||
" movq 32(%rcx), %r12\n movq 40(%rcx), %r13\n movq 48(%rcx), %r14\n movq 56(%rcx), %r15\n"
|
||||
" movups 80(%rcx), %xmm6\n movups 96(%rcx), %xmm7\n movups 112(%rcx), %xmm8\n"
|
||||
" movups 128(%rcx), %xmm9\n movups 144(%rcx), %xmm10\n movups 160(%rcx), %xmm11\n"
|
||||
" movups 176(%rcx), %xmm12\n movups 192(%rcx), %xmm13\n movups 208(%rcx), %xmm14\n"
|
||||
" movups 224(%rcx), %xmm15\n ldmxcsr 240(%rcx)\n fldcw 244(%rcx)\n"
|
||||
" movq 64(%rcx), %rsp\n movl $1, %eax\n jmpq *72(%rcx)\n");
|
||||
|
||||
typedef struct { void *(*entry)(void *); void *argument; } Start;
|
||||
static unsigned __stdcall start(void *p) {
|
||||
Start s = *(Start *)p;
|
||||
free(p);
|
||||
s.entry(s.argument);
|
||||
return 0;
|
||||
}
|
||||
/* The CRT's thread start (per-thread CRT state). The size is committed up front. The thread
|
||||
* starts suspended so *thread is set before it runs. */
|
||||
int host_thread_start(HostThread *thread, size_t stack, void *(*entry)(void *), void *argument) {
|
||||
Start *s = malloc(sizeof(*s));
|
||||
if (!s) return ENOMEM;
|
||||
s->entry = entry; s->argument = argument;
|
||||
uintptr_t handle = _beginthreadex(NULL, (unsigned)stack, start, s, CREATE_SUSPENDED, NULL);
|
||||
if (!handle) { free(s); return EAGAIN; }
|
||||
*thread = (HANDLE)handle;
|
||||
ResumeThread((HANDLE)handle);
|
||||
return 0;
|
||||
}
|
||||
int host_thread_join(HostThread thread) {
|
||||
DWORD r = WaitForSingleObject(thread, INFINITE);
|
||||
CloseHandle(thread);
|
||||
return r == WAIT_OBJECT_0 ? 0 : EINVAL;
|
||||
}
|
||||
void host_thread_detach(HostThread thread) { CloseHandle(thread); }
|
||||
|
||||
static uint64_t qpc_frequency(void) {
|
||||
static uint64_t frequency;
|
||||
if (!frequency) { LARGE_INTEGER f; QueryPerformanceFrequency(&f); frequency = (uint64_t)f.QuadPart; }
|
||||
return frequency;
|
||||
}
|
||||
uint64_t host_monotonic_ns(void) {
|
||||
LARGE_INTEGER c;
|
||||
QueryPerformanceCounter(&c);
|
||||
uint64_t f = qpc_frequency(), n = (uint64_t)c.QuadPart;
|
||||
return n / f * 1000000000u + n % f * 1000000000u / f;
|
||||
}
|
||||
uint64_t host_realtime_ns(void) {
|
||||
FILETIME t;
|
||||
GetSystemTimePreciseAsFileTime(&t);
|
||||
uint64_t ticks = ((uint64_t)t.dwHighDateTime << 32) | t.dwLowDateTime; /* 100 ns since 1601 */
|
||||
return (ticks - UINT64_C(116444736000000000)) * 100;
|
||||
}
|
||||
static __thread HANDLE sleep_timer;
|
||||
/* High-resolution waitable timers (Windows 10 1803+) wake within ~0.5 ms; spin only for the last ~100 us. */
|
||||
void host_sleep_until_ns(uint64_t deadline) {
|
||||
enum { SPIN_NS = 100000 };
|
||||
uint64_t now = host_monotonic_ns();
|
||||
if (now >= deadline) return;
|
||||
if (deadline - now > SPIN_NS) {
|
||||
if (!sleep_timer) {
|
||||
sleep_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
if (!sleep_timer) sleep_timer = CreateWaitableTimerExW(NULL, NULL, 0, TIMER_ALL_ACCESS);
|
||||
}
|
||||
LARGE_INTEGER due;
|
||||
due.QuadPart = -(LONGLONG)((deadline - now - SPIN_NS) / 100);
|
||||
if (sleep_timer && due.QuadPart < 0 && SetWaitableTimer(sleep_timer, &due, 0, NULL, NULL, FALSE))
|
||||
WaitForSingleObject(sleep_timer, INFINITE);
|
||||
else
|
||||
Sleep((DWORD)((deadline - now) / 1000000));
|
||||
}
|
||||
while (host_monotonic_ns() < deadline) YieldProcessor();
|
||||
}
|
||||
void host_sleep_ns(uint64_t ns) {
|
||||
if (!ns) { SwitchToThread(); return; }
|
||||
host_sleep_until_ns(host_monotonic_ns() + ns);
|
||||
}
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
|
||||
int host_thread_start(HostThread *thread, size_t stack, void *(*entry)(void *), void *argument) {
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
if (stack) pthread_attr_setstacksize(&attr, stack);
|
||||
int e = pthread_create(thread, &attr, entry, argument);
|
||||
pthread_attr_destroy(&attr);
|
||||
return e;
|
||||
}
|
||||
int host_thread_join(HostThread thread) { return pthread_join(thread, NULL); }
|
||||
void host_thread_detach(HostThread thread) { pthread_detach(thread); }
|
||||
static uint64_t read_clock(clockid_t id) {
|
||||
struct timespec t;
|
||||
clock_gettime(id, &t);
|
||||
return (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
|
||||
}
|
||||
uint64_t host_monotonic_ns(void) { return read_clock(CLOCK_MONOTONIC); }
|
||||
uint64_t host_realtime_ns(void) { return read_clock(CLOCK_REALTIME); }
|
||||
void host_sleep_ns(uint64_t ns) {
|
||||
struct timespec t = {.tv_sec = (time_t)(ns / 1000000000), .tv_nsec = (long)(ns % 1000000000)};
|
||||
while (nanosleep(&t, &t) && errno == EINTR) {}
|
||||
}
|
||||
void host_sleep_until_ns(uint64_t deadline) {
|
||||
struct timespec t = {(time_t)(deadline / 1000000000u), (long)(deadline % 1000000000u)};
|
||||
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &t, NULL)) {}
|
||||
}
|
||||
#endif
|
||||
+198
-114
@@ -2,20 +2,25 @@
|
||||
* pthread once/keys, signal bookkeeping and a narrow sysctl. Guest values
|
||||
* use FreeBSD numbering; host errno values never reach the guest directly. */
|
||||
#define _GNU_SOURCE
|
||||
#define _CRT_RAND_S
|
||||
#include "runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/random.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/time.h>
|
||||
#include <x86intrin.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <sys/random.h>
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
|
||||
#define ERR(n) ((int32_t)(UINT32_C(0x80020000)|(n)))
|
||||
#define PAGE 16384
|
||||
|
||||
@@ -41,27 +46,47 @@ int32_t runtime_guest_errno(int e) {
|
||||
static int32_t fail_posix(int e) { *runtime_errno()=runtime_guest_errno(e); return -1; }
|
||||
|
||||
/* ---- clocks and sleeping ---- */
|
||||
static struct timespec process_start;
|
||||
__attribute__((constructor)) static void remember_start(void) { clock_gettime(CLOCK_MONOTONIC,&process_start); }
|
||||
static int host_clock(uint32_t id,clockid_t *out) {
|
||||
static uint64_t process_start;
|
||||
__attribute__((constructor)) static void remember_start(void) { process_start=host_monotonic_ns(); }
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef enum { REALTIME, MONOTONIC, THREAD_CPU, PROCESS_CPU } HostClock;
|
||||
#define HOST_CLOCK(linux_id, windows_id) windows_id
|
||||
#else
|
||||
typedef clockid_t HostClock;
|
||||
#define HOST_CLOCK(linux_id, windows_id) linux_id
|
||||
#endif
|
||||
|
||||
static int host_clock(uint32_t id,HostClock *out) {
|
||||
switch (id) {
|
||||
case 0: case 9: case 10: *out=CLOCK_REALTIME; return 1; /* REALTIME(_PRECISE/_FAST) */
|
||||
case 4: case 11: case 12: case 5: case 7: case 8: *out=CLOCK_MONOTONIC; return 1; /* MONOTONIC/UPTIME */
|
||||
case 13: *out=CLOCK_REALTIME_COARSE; return 1; /* SECOND */
|
||||
case 14: *out=CLOCK_THREAD_CPUTIME_ID; return 1;
|
||||
case 2: case 15: *out=CLOCK_PROCESS_CPUTIME_ID; return 1; /* PROF/PROCTIME */
|
||||
case 16: case 17: case 18: case 19: *out=CLOCK_MONOTONIC; return 1; /* PS4 network clocks */
|
||||
case 0: case 9: case 10: *out=HOST_CLOCK(CLOCK_REALTIME,REALTIME); return 1; /* REALTIME(_PRECISE/_FAST) */
|
||||
case 4: case 11: case 12: case 5: case 7: case 8: *out=HOST_CLOCK(CLOCK_MONOTONIC,MONOTONIC); return 1; /* MONOTONIC/UPTIME */
|
||||
case 13: *out=HOST_CLOCK(CLOCK_REALTIME_COARSE,REALTIME); return 1; /* SECOND */
|
||||
case 14: *out=HOST_CLOCK(CLOCK_THREAD_CPUTIME_ID,THREAD_CPU); return 1;
|
||||
case 2: case 15: *out=HOST_CLOCK(CLOCK_PROCESS_CPUTIME_ID,PROCESS_CPU); return 1; /* PROF/PROCTIME */
|
||||
case 16: case 17: case 18: case 19: *out=HOST_CLOCK(CLOCK_MONOTONIC,MONOTONIC); return 1; /* PS4 network clocks */
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int clock_read(uint32_t id,GuestTimespec *ts) {
|
||||
clockid_t host;
|
||||
HostClock host;
|
||||
if (!ts || !host_clock(id,&host)) return EINVAL;
|
||||
#ifdef _WIN32
|
||||
uint64_t ns;
|
||||
if (host==REALTIME) ns=host_realtime_ns();
|
||||
else if (host==MONOTONIC) ns=host_monotonic_ns();
|
||||
else { int64_t user,system; runtime_cpu_times(host==THREAD_CPU,&user,&system); ns=(uint64_t)(user+system)*1000; }
|
||||
ts->sec=(int64_t)(ns/1000000000); ts->nsec=id==13 ? 0 : (int64_t)(ns%1000000000);
|
||||
return 0;
|
||||
#else
|
||||
struct timespec t;
|
||||
if (clock_gettime(host,&t)) return errno;
|
||||
if (id==13) t.tv_nsec=0;
|
||||
ts->sec=t.tv_sec; ts->nsec=t.tv_nsec; return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static ABI int32_t kernel_clock_gettime(uint32_t id,GuestTimespec *ts) {
|
||||
int e=clock_read(id,ts); return e ? ERR(runtime_guest_errno(e)) : 0;
|
||||
}
|
||||
@@ -69,103 +94,81 @@ static ABI int32_t posix_clock_gettime(uint32_t id,GuestTimespec *ts) {
|
||||
int e=clock_read(id,ts); return e ? fail_posix(e) : 0;
|
||||
}
|
||||
static ABI int32_t posix_clock_getres(uint32_t id,GuestTimespec *ts) {
|
||||
clockid_t host; struct timespec t;
|
||||
HostClock host;
|
||||
if (!host_clock(id,&host)) return fail_posix(EINVAL);
|
||||
#ifdef _WIN32
|
||||
if (ts) { ts->sec=0; ts->nsec=100; } /* FILETIME and QPC units */
|
||||
#else
|
||||
struct timespec t;
|
||||
if (clock_getres(host,&t)) return fail_posix(errno);
|
||||
if (ts) { ts->sec=t.tv_sec; ts->nsec=t.tv_nsec; }
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
static ABI uint64_t process_time(void) {
|
||||
struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t);
|
||||
return (uint64_t)(t.tv_sec-process_start.tv_sec)*1000000+(uint64_t)((t.tv_nsec-process_start.tv_nsec)/1000);
|
||||
}
|
||||
static ABI uint64_t process_time_counter(void) {
|
||||
struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t);
|
||||
return (uint64_t)(t.tv_sec-process_start.tv_sec)*1000000000+(uint64_t)(t.tv_nsec-process_start.tv_nsec);
|
||||
}
|
||||
|
||||
static ABI uint64_t process_time(void) { return (host_monotonic_ns()-process_start)/1000; }
|
||||
static ABI uint64_t process_time_counter(void) { return host_monotonic_ns()-process_start; }
|
||||
static ABI uint64_t process_time_frequency(void) { return 1000000000; }
|
||||
static ABI uint64_t read_tsc(void) { return __rdtsc(); }
|
||||
static uint64_t tsc_hz;
|
||||
static ABI uint64_t tsc_frequency(void) {
|
||||
if (!tsc_hz) {
|
||||
struct timespec a,b,nap={0,20000000};
|
||||
clock_gettime(CLOCK_MONOTONIC,&a); uint64_t t0=__rdtsc();
|
||||
nanosleep(&nap,NULL);
|
||||
clock_gettime(CLOCK_MONOTONIC,&b); uint64_t t1=__rdtsc();
|
||||
uint64_t ns=(uint64_t)(b.tv_sec-a.tv_sec)*1000000000+(uint64_t)(b.tv_nsec-a.tv_nsec);
|
||||
uint64_t a=host_monotonic_ns(), t0=__rdtsc();
|
||||
host_sleep_ns(20000000);
|
||||
uint64_t ns=host_monotonic_ns()-a, t1=__rdtsc();
|
||||
tsc_hz=(t1-t0)*1000000000/(ns ? ns : 1);
|
||||
}
|
||||
return tsc_hz;
|
||||
}
|
||||
/* Shared with the GPU library so flip/label timestamps use the guest's clock. */
|
||||
uint64_t runtime_process_time_us(void) { return process_time(); }
|
||||
uint64_t runtime_process_time_counter(void) { return process_time_counter(); }
|
||||
uint64_t runtime_tsc_frequency(void) { return tsc_frequency(); }
|
||||
static int sleep_ns(uint64_t ns) {
|
||||
struct timespec t={.tv_sec=(time_t)(ns/1000000000),.tv_nsec=(long)(ns%1000000000)};
|
||||
while (nanosleep(&t,&t)) if (errno!=EINTR) return errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sleep_ns(uint64_t ns) { host_sleep_ns(ns); return 0; }
|
||||
static ABI int32_t kernel_usleep(uint32_t usec) { sleep_ns((uint64_t)usec*1000); return 0; }
|
||||
static ABI int32_t posix_usleep(uint32_t usec) { sleep_ns((uint64_t)usec*1000); return 0; }
|
||||
static ABI uint32_t posix_sleep(uint32_t seconds) { sleep_ns((uint64_t)seconds*1000000000); return 0; }
|
||||
static ABI int32_t posix_nanosleep(const GuestTimespec *rq,GuestTimespec *rem) {
|
||||
if (!rq || rq->nsec<0 || rq->nsec>=1000000000 || rq->sec<0) return fail_posix(EINVAL);
|
||||
static ABI int32_t kernel_nanosleep(const GuestTimespec *rq,GuestTimespec *rem) {
|
||||
if (!rq || rq->nsec<0 || rq->nsec>=1000000000 || rq->sec<0) return ERR(22);
|
||||
(void)rem;
|
||||
sleep_ns((uint64_t)rq->sec*1000000000+(uint64_t)rq->nsec);
|
||||
if (rem) { rem->sec=0; rem->nsec=0; }
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t kernel_nanosleep(const GuestTimespec *rq,GuestTimespec *rem) {
|
||||
return posix_nanosleep(rq,rem) ? ERR(*runtime_errno()) : 0;
|
||||
static ABI int32_t posix_nanosleep(const GuestTimespec *rq,GuestTimespec *rem) {
|
||||
int32_t r=kernel_nanosleep(rq,rem); return r ? fail_posix(r & 0xffff) : 0;
|
||||
}
|
||||
|
||||
typedef struct { int32_t minuteswest, dsttime; } GuestTimezone;
|
||||
static ABI int32_t kernel_gettimezone(GuestTimezone *tz) {
|
||||
if (!tz) return ERR(22);
|
||||
#ifdef _WIN32
|
||||
tz->minuteswest=(int32_t)(-runtime_utc_offset(time(NULL))/60); tz->dsttime=0;
|
||||
#else
|
||||
time_t now=time(NULL); struct tm local; localtime_r(&now,&local);
|
||||
tz->minuteswest=(int32_t)(-local.tm_gmtoff/60); tz->dsttime=0;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t posix_gettimeofday(GuestTimeval *tv,GuestTimezone *tz) {
|
||||
struct timeval t;
|
||||
gettimeofday(&t,NULL);
|
||||
if (tv) { tv->sec=t.tv_sec; tv->usec=t.tv_usec; }
|
||||
uint64_t now=host_realtime_ns();
|
||||
if (tv) { tv->sec=(int64_t)(now/1000000000); tv->usec=(int64_t)(now%1000000000/1000); }
|
||||
if (tz) kernel_gettimezone(tz);
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t kernel_gettimeofday(GuestTimeval *tv) {
|
||||
if (!tv) return ERR(22);
|
||||
return posix_gettimeofday(tv,NULL);
|
||||
}
|
||||
static ABI int64_t posix_time(int64_t *out) { int64_t t=(int64_t)time(NULL); if (out) *out=t; return t; }
|
||||
|
||||
/* ---- process ---- */
|
||||
static ABI int32_t get_pagesize(void) { return PAGE; }
|
||||
static ABI int32_t get_pid(void) { return 1000; }
|
||||
static ABI int32_t yield(void) { sched_yield(); return 0; }
|
||||
static ABI int32_t yield(void) { host_yield(); return 0; }
|
||||
static ABI __attribute__((noreturn)) void hard_exit(int status) {
|
||||
printf("Runtime: guest requested _exit(%d)\n",status);
|
||||
runtime_report();
|
||||
fflush(stdout);
|
||||
_exit(status);
|
||||
}
|
||||
static ABI __attribute__((noreturn)) void raise_exception(uint32_t code,uint64_t argument) {
|
||||
fprintf(stderr,"STOP: guest raised debug exception code=0x%x argument=0x%llx\n",code,(unsigned long long)argument);
|
||||
runtime_report();
|
||||
exit(23);
|
||||
}
|
||||
static ABI int32_t print_backtrace(void) {
|
||||
fputs("Runtime: guest requested a backtrace (not available)\n",stderr);
|
||||
return 0;
|
||||
exit(status);
|
||||
}
|
||||
|
||||
typedef struct { uint32_t bits[4]; } GuestSigset;
|
||||
static _Thread_local GuestSigset signal_mask;
|
||||
static uintptr_t handlers[128];
|
||||
static ABI uintptr_t guest_signal(int sig,uintptr_t handler) {
|
||||
if (sig<=0 || sig>=128) { *runtime_errno()=22; return (uintptr_t)-1; } /* SIG_ERR */
|
||||
uintptr_t old=handlers[sig]; handlers[sig]=handler;
|
||||
printf("Runtime: guest signal(%d) handler recorded (host signals are not forwarded)\n",sig);
|
||||
return old;
|
||||
}
|
||||
static ABI int32_t guest_sigprocmask(int how,const GuestSigset *set,GuestSigset *old) {
|
||||
if (old) *old=signal_mask;
|
||||
if (!set) return 0;
|
||||
@@ -181,25 +184,53 @@ static ABI int32_t guest_sigfillset(GuestSigset *set) { if (!set) return fail_po
|
||||
static ABI int32_t guest_sigemptyset(GuestSigset *set) { if (!set) return fail_posix(EINVAL); memset(set,0,sizeof(*set)); return 0; }
|
||||
typedef struct { GuestTimeval utime, stime; int64_t rest[14]; } GuestRusage;
|
||||
static ABI int32_t guest_getrusage(int who,GuestRusage *out) {
|
||||
struct rusage r;
|
||||
if (!out || (who!=0 && who!=1)) return fail_posix(EINVAL);
|
||||
getrusage(who==0 ? RUSAGE_SELF : RUSAGE_THREAD,&r);
|
||||
memset(out,0,sizeof(*out));
|
||||
#ifdef _WIN32
|
||||
int64_t user,system;
|
||||
runtime_cpu_times(who==1,&user,&system);
|
||||
out->utime=(GuestTimeval){user/1000000,user%1000000};
|
||||
out->stime=(GuestTimeval){system/1000000,system%1000000};
|
||||
#else
|
||||
struct rusage r;
|
||||
getrusage(who==0 ? RUSAGE_SELF : RUSAGE_THREAD,&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};
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t get_host_ncpu(void) {
|
||||
const char *env = getenv("BB_NCPU");
|
||||
if (env && *env) {
|
||||
int v = atoi(env);
|
||||
if (v > 0) return (int32_t)v;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
SYSTEM_INFO si;
|
||||
GetSystemInfo(&si);
|
||||
return si.dwNumberOfProcessors > 0 ? (int32_t)si.dwNumberOfProcessors : 4;
|
||||
#else
|
||||
long n = sysconf(_SC_NPROCESSORS_ONLN);
|
||||
return n > 0 ? (int32_t)n : 4;
|
||||
#endif
|
||||
}
|
||||
|
||||
static ABI int32_t guest_sysctl(const int32_t *name,uint32_t namelen,void *old,uint64_t *oldlen,const void *new_value,uint64_t newlen) {
|
||||
(void)newlen;
|
||||
if (!name || namelen<2 || new_value) return fail_posix(EINVAL);
|
||||
if (name[0]==1 && name[1]==37) { /* kern.arandom */
|
||||
if (!old || !oldlen) return fail_posix(EINVAL);
|
||||
#ifdef _WIN32
|
||||
if (runtime_random(old,(size_t)*oldlen)<0) return fail_posix(errno);
|
||||
#else
|
||||
if (getrandom(old,(size_t)*oldlen,0)<0) return fail_posix(errno);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
if (name[0]==6 && (name[1]==7 || name[1]==3)) { /* hw.pagesize / hw.ncpu */
|
||||
if (!oldlen) return fail_posix(EINVAL);
|
||||
int32_t value=name[1]==7 ? PAGE : 7;
|
||||
int32_t value = (name[1] == 7) ? PAGE : get_host_ncpu();
|
||||
if (old) { if (*oldlen<4) return fail_posix(ENOMEM); memcpy(old,&value,4); }
|
||||
*oldlen=4; return 0;
|
||||
}
|
||||
@@ -220,83 +251,136 @@ static ABI int32_t thread_once(int32_t *once,void (ABI *routine)(void)) {
|
||||
__atomic_store_n(once,1,__ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
sched_yield();
|
||||
host_yield();
|
||||
}
|
||||
}
|
||||
static ABI int32_t posix_once(int32_t *once,void (ABI *routine)(void)) { return thread_once(once,routine) ? 22 : 0; }
|
||||
#define KEYS 256
|
||||
typedef void (ABI *KeyDestructor)(void *);
|
||||
static struct { int used; KeyDestructor destructor; } keys[KEYS];
|
||||
static pthread_mutex_t key_lock=PTHREAD_MUTEX_INITIALIZER;
|
||||
static HostMutex key_lock = HOST_MUTEX_INIT;
|
||||
static _Thread_local void *key_values[KEYS];
|
||||
|
||||
static ABI int32_t key_create(uint32_t *key,KeyDestructor destructor) {
|
||||
if (!key) return ERR(22);
|
||||
pthread_mutex_lock(&key_lock);
|
||||
host_lock(&key_lock);
|
||||
for (uint32_t i=1;i<KEYS;++i) if (!keys[i].used) {
|
||||
keys[i].used=1; keys[i].destructor=destructor;
|
||||
pthread_mutex_unlock(&key_lock);
|
||||
host_unlock(&key_lock);
|
||||
*key=i; return 0;
|
||||
}
|
||||
pthread_mutex_unlock(&key_lock);
|
||||
host_unlock(&key_lock);
|
||||
return ERR(35);
|
||||
}
|
||||
static ABI int32_t key_delete(uint32_t key) {
|
||||
if (!key || key>=KEYS) return ERR(22);
|
||||
pthread_mutex_lock(&key_lock);
|
||||
host_lock(&key_lock);
|
||||
int32_t r=keys[key].used ? 0 : ERR(22);
|
||||
keys[key].used=0; keys[key].destructor=NULL;
|
||||
pthread_mutex_unlock(&key_lock);
|
||||
host_unlock(&key_lock);
|
||||
return r;
|
||||
}
|
||||
static ABI int32_t key_set(uint32_t key,void *value) {
|
||||
if (!key || key>=KEYS || !keys[key].used) return ERR(22);
|
||||
key_values[key]=value; return 0;
|
||||
}
|
||||
static ABI void *key_get(uint32_t key) { return key && key<KEYS ? key_values[key] : NULL; }
|
||||
static ABI void *key_get(uint32_t key) { return (!key || key>=KEYS || !keys[key].used) ? NULL : key_values[key]; }
|
||||
static ABI int32_t posix_key_create(uint32_t *key,KeyDestructor destructor) {
|
||||
int32_t r=key_create(key,destructor); return r ? (int32_t)(r & 0xffff) : 0;
|
||||
}
|
||||
static ABI int32_t posix_key_delete(uint32_t key) {
|
||||
int32_t r=key_delete(key); return r ? (int32_t)(r & 0xffff) : 0;
|
||||
}
|
||||
static ABI int32_t posix_key_set(uint32_t key,void *value) {
|
||||
int32_t r=key_set(key,value); return r ? (int32_t)(r & 0xffff) : 0;
|
||||
}
|
||||
|
||||
void runtime_thread_keys_cleanup(void) {
|
||||
for (int round=0;round<4;++round) {
|
||||
int any=0;
|
||||
for (int iter=0;iter<4;++iter) {
|
||||
int found=0;
|
||||
for (uint32_t i=1;i<KEYS;++i) {
|
||||
void *value=key_values[i];
|
||||
if (!value || !keys[i].used || !keys[i].destructor) continue;
|
||||
key_values[i]=NULL; any=1;
|
||||
keys[i].destructor(value);
|
||||
void *v=key_values[i];
|
||||
if (!v) continue;
|
||||
key_values[i]=NULL;
|
||||
KeyDestructor d=keys[i].used ? keys[i].destructor : NULL;
|
||||
if (d) { d(v); found=1; }
|
||||
}
|
||||
if (!any) break;
|
||||
if (!found) break;
|
||||
}
|
||||
}
|
||||
static ABI int32_t posix_key_create(uint32_t *k,KeyDestructor d) { int32_t r=key_create(k,d); return r ? r&0xffff : 0; }
|
||||
static ABI int32_t posix_key_delete(uint32_t k) { int32_t r=key_delete(k); return r ? r&0xffff : 0; }
|
||||
static ABI int32_t posix_key_set(uint32_t k,void *v) { int32_t r=key_set(k,v); return r ? r&0xffff : 0; }
|
||||
|
||||
static const RuntimeExport exports[]={
|
||||
{"sceKernelClockGettime",kernel_clock_gettime}, {"clock_gettime",posix_clock_gettime},
|
||||
{"clock_getres",posix_clock_getres},
|
||||
{"sceKernelGetProcessTime",process_time}, {"sceKernelGetProcessTimeCounter",process_time_counter},
|
||||
{"sceKernelGetProcessTimeCounterFrequency",process_time_frequency},
|
||||
{"sceKernelReadTsc",read_tsc}, {"sceKernelGetTscFrequency",tsc_frequency},
|
||||
{"sceKernelUsleep",kernel_usleep}, {"usleep",posix_usleep}, {"sleep",posix_sleep},
|
||||
{"nanosleep",posix_nanosleep}, {"sceKernelNanosleep",kernel_nanosleep},
|
||||
{"sceKernelGettimezone",kernel_gettimezone}, {"gettimeofday",posix_gettimeofday},
|
||||
{"sceKernelGettimeofday",kernel_gettimeofday},
|
||||
/* Bloodborne NIDs */
|
||||
{"QBi7HCK03hw#p#J",kernel_clock_gettime},
|
||||
{"1jfXLRVzisc#p#J",kernel_usleep},
|
||||
{"-2IRUCO--PM#p#J",read_tsc},
|
||||
{"4J2sUJmuHZQ#p#J",process_time},
|
||||
{"kOcnerypnQA#p#J",kernel_gettimezone},
|
||||
{"yS8U2TGCe1A#p#J",kernel_nanosleep},
|
||||
{"6XG4B33N09g#p#J",yield},
|
||||
{"T72hz6ffq08#p#J",yield},
|
||||
{"6Z83sYWFlA8#p#J",hard_exit},
|
||||
{"3kg7rT0NQIs#p#J",hard_exit},
|
||||
{"ejekcaNQNq0#p#J",posix_gettimeofday},
|
||||
{"lLMT9vJAck0#p#J",posix_clock_gettime},
|
||||
{"n88vx3C5nW8#p#J",posix_gettimeofday},
|
||||
{"n88vx3C5nW8#I#J",posix_gettimeofday},
|
||||
{"wLlFkwG9UcQ#q#q",posix_time},
|
||||
{"QcteRwbsnV0#I#J",posix_usleep},
|
||||
{"0wu33hunNdE#I#J",posix_sleep},
|
||||
{"FJrT5LuUBAU#I#J",hard_exit},
|
||||
{"DFmMT80xcNI#p#J",guest_sysctl},
|
||||
|
||||
/* Alternate NIDs & plain symbol names */
|
||||
{"k5cZ6eB0GjU#p#J",kernel_clock_gettime},
|
||||
{"qZ7nU89W8s4#p#J",posix_clock_gettime},
|
||||
{"Hq79wG3lF8I#p#J",posix_clock_getres},
|
||||
{"8gC35w1n37s#p#J",process_time},
|
||||
{"K84gq3HqC44#p#J",process_time_counter},
|
||||
{"6q5j7uQ+6g4#p#J",process_time_frequency},
|
||||
{"7J7CqZ-X-Z0#p#J",read_tsc},
|
||||
{"t+993k+39C0#p#J",tsc_frequency},
|
||||
{"-GqRms9mF9Q#p#J",kernel_usleep},
|
||||
{"7sNqP8Z-k8I#p#J",posix_usleep},
|
||||
{"Gq6nQ9e2zGg#p#J",posix_sleep},
|
||||
{"v6Y4zB2uM-g#p#J",kernel_nanosleep},
|
||||
{"ZgE5+j47X1g#p#J",posix_nanosleep},
|
||||
{"4y4hJj5M81A#p#J",kernel_gettimezone},
|
||||
{"k1mP8Y-f1vQ#p#J",posix_gettimeofday},
|
||||
{"6Q9j3fL6X8A#p#J",posix_time},
|
||||
{"1q6u7B7G33k#p#J",get_pagesize},
|
||||
{"e2X73+L7-i4#p#J",get_pid},
|
||||
{"3u2F0q3wYgI#p#J",yield},
|
||||
{"u4K7G8-B8hA#p#J",hard_exit},
|
||||
{"2N6s4+z25eY#p#J",guest_sigprocmask},
|
||||
{"5z8k3hP9P8s#p#J",guest_sigfillset},
|
||||
{"1F3m-X-k88I#p#J",guest_sigemptyset},
|
||||
{"X8K43+7wP-s#p#J",guest_getrusage},
|
||||
{"1gK2F-X58+4#p#J",guest_sysctl},
|
||||
{"7b4h-48Kq3w#p#J",thread_once},
|
||||
{"0W-jP5E2KjQ#p#J",posix_once},
|
||||
{"9k2j0+T7-N4#p#J",key_create},
|
||||
{"2B8q1pB1H+I#p#J",key_delete},
|
||||
{"E97h4n4w7z8#p#J",key_set},
|
||||
{"f+8F99p7v5g#p#J",key_get},
|
||||
|
||||
/* Symbol names */
|
||||
{"sceKernelClockGettime",kernel_clock_gettime},
|
||||
{"clock_gettime",posix_clock_gettime},
|
||||
{"sceKernelReadTsc",read_tsc},
|
||||
{"sceKernelUsleep",kernel_usleep},
|
||||
{"usleep",posix_usleep},
|
||||
{"sleep",posix_sleep},
|
||||
{"nanosleep",posix_nanosleep},
|
||||
{"sched_yield",yield},
|
||||
{"scePthreadYield",yield},
|
||||
{"gettimeofday",posix_gettimeofday},
|
||||
{"time",posix_time},
|
||||
{"getpagesize",get_pagesize}, {"getpid",get_pid}, {"sched_yield",yield},
|
||||
{"_exit",hard_exit},
|
||||
{"sceKernelDebugRaiseException",raise_exception},
|
||||
{"sceKernelDebugRaiseExceptionOnReleaseMode",raise_exception},
|
||||
{"sceKernelPrintBacktraceWithModuleInfo",print_backtrace},
|
||||
{"signal",guest_signal}, {"sigprocmask",guest_sigprocmask}, {"_sigprocmask",guest_sigprocmask},
|
||||
{"sigfillset",guest_sigfillset}, {"sigemptyset",guest_sigemptyset},
|
||||
{"getrusage",guest_getrusage}, {"sysctl",guest_sysctl},
|
||||
{"scePthreadOnce",thread_once}, {"pthread_once",posix_once},
|
||||
{"scePthreadKeyCreate",key_create}, {"scePthreadKeyDelete",key_delete},
|
||||
{"scePthreadSetspecific",key_set}, {"scePthreadGetspecific",key_get},
|
||||
{"pthread_key_create",posix_key_create}, {"pthread_key_delete",posix_key_delete},
|
||||
{"pthread_setspecific",posix_key_set}, {"pthread_getspecific",key_get},
|
||||
{"sysctl",guest_sysctl},
|
||||
{"pthread_key_create",posix_key_create},
|
||||
{"pthread_key_delete",posix_key_delete},
|
||||
{"pthread_setspecific",posix_key_set},
|
||||
{"pthread_getspecific",key_get},
|
||||
};
|
||||
|
||||
uintptr_t runtime_kernel_resolve(const char *name) { return RUNTIME_LOOKUP(exports,name); }
|
||||
#else
|
||||
uintptr_t runtime_kernel_resolve(const char *name) { (void)name; return 0; }
|
||||
int32_t runtime_guest_errno(int e) { return e ? 5 : 0; }
|
||||
void runtime_thread_keys_cleanup(void) {}
|
||||
#endif
|
||||
+290
-27
@@ -10,13 +10,15 @@
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <errno.h>
|
||||
#ifdef _WIN32
|
||||
#include "win32_memory.h"
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
/* sceKernelGetDirectMemorySize on retail PS4: 5056 MiB. BB_DMEM_MB raises it (the resolution
|
||||
* patches above 1080p need about 4 GiB more; run.sh sets it). */
|
||||
static uint64_t pool_size_bytes(void) {
|
||||
@@ -52,29 +54,61 @@ typedef struct { void *start; uint64_t offset, length; int8_t prot, type; int16_
|
||||
_Static_assert(sizeof(VirtualQueryInfo)==72,"PS4 virtual query info layout");
|
||||
_Static_assert(sizeof(BatchEntry)==32,"PS4 batch map entry layout");
|
||||
|
||||
/* Region table lock. Lookups (GPU per-draw queries, VirtualQuery, GPU page
|
||||
* protection) share it; map/unmap/protect take it exclusively. Both nest: code
|
||||
* under the lock may write guest memory, and a GPU-tracked page then faults into
|
||||
* runtime_memory_gpu_protect on the same thread. Readers nest because glibc's
|
||||
* default rwlock prefers readers; a thread holding it exclusively skips relocking. */
|
||||
static pthread_rwlock_t lock=PTHREAD_RWLOCK_INITIALIZER;
|
||||
static __thread unsigned exclusive_depth;
|
||||
/* Odd while a writer holds the lock; readers' region caches are valid for one even value. */
|
||||
/* Region table lock. Nested reads are counted per thread. */
|
||||
static HostRwlock lock=HOST_RWLOCK_INIT;
|
||||
static __thread unsigned exclusive_depth, shared_depth;
|
||||
static uint64_t table_generation;
|
||||
static void write_lock(void) {
|
||||
if (!exclusive_depth++) { pthread_rwlock_wrlock(&lock); __atomic_add_fetch(&table_generation,1,__ATOMIC_ACQ_REL); }
|
||||
if (!exclusive_depth++) { host_write_lock(&lock); __atomic_add_fetch(&table_generation,1,__ATOMIC_ACQ_REL); }
|
||||
}
|
||||
static void write_unlock(void) {
|
||||
if (!--exclusive_depth) { __atomic_add_fetch(&table_generation,1,__ATOMIC_RELEASE); pthread_rwlock_unlock(&lock); }
|
||||
if (!--exclusive_depth) { __atomic_add_fetch(&table_generation,1,__ATOMIC_RELEASE); host_write_unlock(&lock); }
|
||||
}
|
||||
static void read_lock(void) { if (!exclusive_depth) pthread_rwlock_rdlock(&lock); }
|
||||
static void read_unlock(void) { if (!exclusive_depth) pthread_rwlock_unlock(&lock); }
|
||||
static void read_lock(void) { if (!exclusive_depth && !shared_depth++) host_read_lock(&lock); }
|
||||
static void read_unlock(void) { if (!exclusive_depth && !--shared_depth) host_read_unlock(&lock); }
|
||||
static Block blocks[LIMIT];
|
||||
static Vma *vmas; static size_t vma_count, vma_capacity;
|
||||
static int pool_fd=-1;
|
||||
static unsigned char *backing_base; /* second view of the pool: host writes bypass guest/GPU page protection */
|
||||
#define FLEX_SPAN (UINT64_C(1024) * 1024 * 1024)
|
||||
static uint64_t flex_bitmap[FLEX_SPAN/PAGE/64];
|
||||
|
||||
/* Recent memory operations ring buffer for crash diagnostics */
|
||||
typedef struct {
|
||||
uint64_t tick;
|
||||
int kind; /* 1: map, 2: unmap, 3: protect, 4: release */
|
||||
uintptr_t addr;
|
||||
uint64_t size;
|
||||
int prot;
|
||||
uint32_t tid;
|
||||
} MemOpLog;
|
||||
static MemOpLog g_mem_op_ring[64];
|
||||
static uint32_t g_mem_op_count = 0;
|
||||
static void log_mem_op(int kind, uintptr_t addr, uint64_t size, int prot) {
|
||||
uint32_t idx = __atomic_fetch_add(&g_mem_op_count, 1, __ATOMIC_RELAXED);
|
||||
MemOpLog *e = &g_mem_op_ring[idx & 63];
|
||||
e->tick = host_monotonic_ns();
|
||||
e->kind = kind;
|
||||
e->addr = addr;
|
||||
e->size = size;
|
||||
e->prot = prot;
|
||||
e->tid = (uint32_t)host_thread_id();
|
||||
}
|
||||
void runtime_memory_dump_recent_ops(void *file_handle) {
|
||||
FILE *f = (FILE *)file_handle;
|
||||
if (!f) return;
|
||||
fprintf(f, "\n--- LAST 64 MEMORY OPERATIONS ---\n");
|
||||
uint32_t total = __atomic_load_n(&g_mem_op_count, __ATOMIC_RELAXED);
|
||||
uint32_t count = total < 64 ? total : 64;
|
||||
uint32_t start = total > 64 ? (total - 64) : 0;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
MemOpLog *e = &g_mem_op_ring[(start + i) & 63];
|
||||
const char *k = e->kind == 1 ? "MAP" : e->kind == 2 ? "UNMAP" : e->kind == 3 ? "PROTECT" : "RELEASE";
|
||||
fprintf(f, " #%2u: [%7s] addr=0x%" PRIxPTR ", size=0x%" PRIx64 ", prot=0x%x (tid=%u, tick=%" PRIu64 ")\n",
|
||||
i + 1, k, e->addr, e->size, e->prot, e->tid, e->tick);
|
||||
}
|
||||
}
|
||||
|
||||
/* GPU hooks (bbgpu): notified outside the lock, in order, after each operation. */
|
||||
typedef void (*GpuRange)(uintptr_t address, uint64_t size);
|
||||
static GpuRange hook_map, hook_unmap, hook_invalidate;
|
||||
@@ -87,18 +121,41 @@ static uint64_t live_bytes, flexible_bytes;
|
||||
static int valid_alignment(uint64_t a) { return a >= PAGE && a <= POOL_SIZE && !(a & (a-1)); }
|
||||
static uint64_t align_up(uint64_t n, uint64_t a) { return (n+a-1) & ~(a-1); }
|
||||
static int host_prot(int prot) {
|
||||
/* GPU read/write bits (0x10/0x20) need host access for the future GPU backend. */
|
||||
return ((prot & 0x11) ? PROT_READ : 0) | ((prot & 0x22) ? PROT_WRITE|PROT_READ : 0) | ((prot & 4) ? PROT_EXEC|PROT_READ : 0);
|
||||
}
|
||||
/* Direct memory occupies [0,POOL_SIZE) of the memfd, flexible memory [POOL_SIZE,+FLEX_SPAN). */
|
||||
#ifdef _WIN32
|
||||
#ifndef LOW_MIN
|
||||
#define LOW_MIN UINT64_C(0x0800000000)
|
||||
#endif
|
||||
static int space(void) { return win_mem_space(LOW_MIN,USER_MAX); }
|
||||
int runtime_memory_reserve_space(void) { return space(); }
|
||||
#else
|
||||
int runtime_memory_reserve_space(void) { return 0; }
|
||||
#endif
|
||||
static int pool(void) {
|
||||
if (pool_fd>=0) return 0;
|
||||
#ifdef _WIN32
|
||||
void *view = NULL;
|
||||
if (space() || win_mem_section(POOL_SIZE+FLEX_SPAN,&view)) return -1;
|
||||
backing_base = (unsigned char *)view; pool_fd = 0;
|
||||
const char *selftest = getenv("BB_SELFTEST_MEM");
|
||||
if (selftest && selftest[0] == '1') win_mem_run_selftest();
|
||||
return 0;
|
||||
#else
|
||||
pool_fd=memfd_create("bb-guest-memory", MFD_CLOEXEC);
|
||||
if (pool_fd<0 || ftruncate(pool_fd,(off_t)(POOL_SIZE+FLEX_SPAN))) return -1;
|
||||
void *view=mmap(NULL,POOL_SIZE+FLEX_SPAN,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_NORESERVE,pool_fd,0);
|
||||
if (view==MAP_FAILED) return -1;
|
||||
backing_base=view;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
static void punch(uint64_t phys, uint64_t size) {
|
||||
#ifdef _WIN32
|
||||
win_mem_punch(phys, size);
|
||||
#else
|
||||
fallocate(pool_fd,FALLOC_FL_PUNCH_HOLE|FALLOC_FL_KEEP_SIZE,(off_t)phys,(off_t)size);
|
||||
#endif
|
||||
}
|
||||
static void queue_hook(int kind, uintptr_t address, uint64_t size) {
|
||||
GpuRange hook = kind==HOOK_MAP ? hook_map : kind==HOOK_UNMAP ? hook_unmap : hook_invalidate;
|
||||
@@ -134,7 +191,11 @@ static uint64_t flex_alloc(uint64_t size) {
|
||||
}
|
||||
static void flex_free(uint64_t phys, uint64_t size) {
|
||||
flex_set((phys-POOL_SIZE)/PAGE,size/PAGE,0);
|
||||
#ifdef _WIN32
|
||||
if (backing_base) memset(backing_base+phys,0,size);
|
||||
#else
|
||||
fallocate(pool_fd,FALLOC_FL_PUNCH_HOLE|FALLOC_FL_KEEP_SIZE,(off_t)phys,(off_t)size);
|
||||
#endif
|
||||
}
|
||||
static size_t vma_index(uintptr_t a) { /* first VMA with end > a */
|
||||
size_t lo=0, hi=vma_count;
|
||||
@@ -201,25 +262,47 @@ static void drop_range(uintptr_t start, uintptr_t end) {
|
||||
}
|
||||
vma_erase(i,n);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
/* A view piece remapped around a removed range (win32_memory.c) gets its pages' guest
|
||||
* protection back; the GPU forgets its page tracking state there. Runs before the table
|
||||
* changes, so the regions still describe the piece. */
|
||||
static void restore_remnant(uintptr_t start, uintptr_t end) {
|
||||
for (size_t i=vma_index(start); i<vma_count && vmas[i].start<end; ++i) {
|
||||
if (vmas[i].kind==KIND_RESERVED) continue;
|
||||
uintptr_t a=vmas[i].start>start ? vmas[i].start : start, b=vmas[i].end<end ? vmas[i].end : end;
|
||||
win_mem_protect(a,b,host_prot(vmas[i].prot));
|
||||
}
|
||||
queue_hook(HOOK_INVALIDATE,start,end-start);
|
||||
}
|
||||
#endif
|
||||
/* Places a host mapping; with MAP_FIXED it replaces what the guest had there. */
|
||||
static int32_t place(void **inout, uint64_t size, int prot, int flags, uint64_t alignment,
|
||||
int kind, int type, uint64_t phys) {
|
||||
uintptr_t address=(uintptr_t)*inout;
|
||||
if (flags & MAP_FIXED_FLAG) {
|
||||
if (!address || address%PAGE) return INVALID;
|
||||
if (address < USER_MIN || address + size > USER_MAX) return INVALID;
|
||||
if ((flags & MAP_NO_OVERWRITE) && overlaps(address,address+size,1)) return NO_MEMORY;
|
||||
} else {
|
||||
address=find_free(address,size,alignment);
|
||||
if (!address) return NO_MEMORY;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (space()) return NO_MEMORY;
|
||||
if (kind!=KIND_RESERVED ? win_mem_map(address,address+size,phys,host_prot(prot),restore_remnant)
|
||||
: win_mem_release(address,address+size,restore_remnant)) return NO_MEMORY;
|
||||
void *mapped=(void *)address;
|
||||
#else
|
||||
void *mapped = kind!=KIND_RESERVED
|
||||
? mmap((void *)address,size,host_prot(prot),MAP_SHARED|MAP_FIXED,pool_fd,(off_t)phys)
|
||||
: mmap((void *)address,size,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE|MAP_FIXED,-1,0);
|
||||
if (mapped==MAP_FAILED) return NO_MEMORY;
|
||||
#endif
|
||||
drop_range(address,address+size);
|
||||
if (vma_insert(vma_index(address),(Vma){address,address+size,kind,prot,type,phys})) return NO_MEMORY;
|
||||
if (kind==KIND_FLEXIBLE) flexible_bytes+=size;
|
||||
if (kind!=KIND_RESERVED) queue_hook(HOOK_MAP,address,size);
|
||||
log_mem_op(1, address, size, prot);
|
||||
*inout=mapped;
|
||||
return 0;
|
||||
}
|
||||
@@ -298,10 +381,16 @@ static int32_t unmap_locked(uintptr_t start, uint64_t size) {
|
||||
uintptr_t a=vmas[i].start>start ? vmas[i].start : start, b=vmas[i].end<end ? vmas[i].end : end;
|
||||
queue_hook(HOOK_UNMAP,a,b-a);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (vma_index(start)<vma_count && vmas[vma_index(start)].start<end && win_mem_release(start,end,restore_remnant))
|
||||
return INVALID;
|
||||
#else
|
||||
for (size_t i=vma_index(start); i<vma_count && vmas[i].start<end; ++i) {
|
||||
uintptr_t a=vmas[i].start>start ? vmas[i].start : start, b=vmas[i].end<end ? vmas[i].end : end;
|
||||
if (munmap((void *)a,b-a)) return INVALID;
|
||||
}
|
||||
#endif
|
||||
log_mem_op(2, start, size, 0);
|
||||
drop_range(start,end);
|
||||
return 0;
|
||||
}
|
||||
@@ -343,7 +432,8 @@ static ABI int32_t direct_release(uint64_t start, uint64_t size) {
|
||||
if (right.size) for (int j=0;j<LIMIT;++j) if (!blocks[j].used) { blocks[j]=right; break; }
|
||||
live_bytes-=e-a;
|
||||
}
|
||||
fallocate(pool_fd,FALLOC_FL_PUNCH_HOLE|FALLOC_FL_KEEP_SIZE,(off_t)start,(off_t)size); /* zero on reuse */
|
||||
punch(start,size); /* zero on reuse */
|
||||
log_mem_op(4, start, size, 0);
|
||||
write_unlock();
|
||||
flush_hooks();
|
||||
return 0;
|
||||
@@ -387,11 +477,16 @@ static int32_t protect_locked(uintptr_t start, uint64_t size, int prot, int type
|
||||
uintptr_t end=start+align_up(size,PAGE);
|
||||
start&=~(uintptr_t)(PAGE-1);
|
||||
if ((prot & ~0x37) || !covered(start,end,0)) return INVALID;
|
||||
#ifdef _WIN32
|
||||
if (win_mem_protect(start,end,host_prot(prot))) return INVALID;
|
||||
#else
|
||||
if (mprotect((void *)start,end-start,host_prot(prot))) return INVALID;
|
||||
#endif
|
||||
int error; size_t i=carve(start,end,&error);
|
||||
if (error) return NO_MEMORY;
|
||||
for (; i<vma_count && vmas[i].start<end; ++i) { vmas[i].prot=prot; if (type>=0) vmas[i].type=type; }
|
||||
queue_hook(HOOK_INVALIDATE,start,end-start);
|
||||
log_mem_op(3, start, size, prot);
|
||||
++protects;
|
||||
return 0;
|
||||
}
|
||||
@@ -491,11 +586,26 @@ uintptr_t runtime_memory_resolve(const char *name) {
|
||||
}
|
||||
/* Host-owned memory the guest can see (image, stacks, trampolines) lives in
|
||||
* [LOW_MIN, USER_MIN): PS4 code packs pointers into 40-bit fields. */
|
||||
#ifndef LOW_MIN
|
||||
#define LOW_MIN UINT64_C(0x0800000000)
|
||||
#endif
|
||||
static uintptr_t low_next=LOW_MIN;
|
||||
void *runtime_low_map(size_t size, int prot) {
|
||||
size=align_up(size,PAGE);
|
||||
write_lock();
|
||||
#ifdef _WIN32
|
||||
/* Read/write; the loader changes protection afterwards (VirtualProtect). */
|
||||
(void)prot;
|
||||
void *p=NULL;
|
||||
if (!space())
|
||||
while (!p && low_next+size<=USER_MIN) {
|
||||
p=win_mem_private(low_next,low_next+size);
|
||||
low_next+=size+PAGE; /* reserved gap catches overruns */
|
||||
}
|
||||
write_unlock();
|
||||
CHECK_LOW_ADDR(p);
|
||||
return p;
|
||||
#else
|
||||
void *p=MAP_FAILED;
|
||||
while (low_next+size<=USER_MIN) {
|
||||
p=mmap((void *)low_next,size,prot,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE,-1,0);
|
||||
@@ -503,7 +613,123 @@ void *runtime_low_map(size_t size, int prot) {
|
||||
if (p!=MAP_FAILED) break;
|
||||
}
|
||||
write_unlock();
|
||||
CHECK_LOW_ADDR(p);
|
||||
return p==MAP_FAILED ? NULL : p;
|
||||
#endif
|
||||
}
|
||||
|
||||
typedef struct LowHeader {
|
||||
uint32_t magic;
|
||||
uint32_t size_class;
|
||||
uint64_t total_size;
|
||||
} LowHeader;
|
||||
|
||||
typedef struct LowFreeNode {
|
||||
struct LowFreeNode *next;
|
||||
} LowFreeNode;
|
||||
|
||||
static const size_t low_classes[] = { 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536 };
|
||||
#define NUM_LOW_CLASSES (sizeof(low_classes)/sizeof(low_classes[0]))
|
||||
static LowFreeNode *low_freelists[NUM_LOW_CLASSES];
|
||||
static HostMutex low_alloc_lock = HOST_MUTEX_INIT;
|
||||
|
||||
void *runtime_low_alloc(size_t size) {
|
||||
if (!size) size = 1;
|
||||
size_t needed = size + sizeof(LowHeader);
|
||||
int sc = -1;
|
||||
for (size_t i = 0; i < NUM_LOW_CLASSES; ++i) {
|
||||
if (low_classes[i] >= needed) {
|
||||
sc = (int)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sc != -1) {
|
||||
host_mutex_lock(&low_alloc_lock);
|
||||
LowFreeNode *node = low_freelists[sc];
|
||||
if (node) {
|
||||
low_freelists[sc] = node->next;
|
||||
host_mutex_unlock(&low_alloc_lock);
|
||||
} else {
|
||||
size_t block_size = low_classes[sc];
|
||||
size_t slab_size = 65536;
|
||||
if (slab_size < block_size * 4) slab_size = block_size * 4;
|
||||
slab_size = align_up(slab_size, PAGE);
|
||||
unsigned char *slab = (unsigned char *)runtime_low_map(slab_size, PROT_READ | PROT_WRITE);
|
||||
if (!slab) {
|
||||
host_mutex_unlock(&low_alloc_lock);
|
||||
return NULL;
|
||||
}
|
||||
node = (LowFreeNode *)slab;
|
||||
size_t count = slab_size / block_size;
|
||||
LowFreeNode *head = NULL;
|
||||
for (size_t j = 1; j < count; ++j) {
|
||||
LowFreeNode *n = (LowFreeNode *)(slab + j * block_size);
|
||||
n->next = head;
|
||||
head = n;
|
||||
}
|
||||
low_freelists[sc] = head;
|
||||
host_mutex_unlock(&low_alloc_lock);
|
||||
}
|
||||
LowHeader *hdr = (LowHeader *)node;
|
||||
hdr->magic = 0x4C4F5741;
|
||||
hdr->size_class = (uint32_t)sc;
|
||||
hdr->total_size = low_classes[sc];
|
||||
void *ret = (void *)(hdr + 1);
|
||||
CHECK_LOW_ADDR(ret);
|
||||
return ret;
|
||||
} else {
|
||||
size_t map_size = align_up(needed, PAGE);
|
||||
void *p = runtime_low_map(map_size, PROT_READ | PROT_WRITE);
|
||||
if (!p) return NULL;
|
||||
LowHeader *hdr = (LowHeader *)p;
|
||||
hdr->magic = 0x4C4F5741;
|
||||
hdr->size_class = 0xFFFFFFFF;
|
||||
hdr->total_size = map_size;
|
||||
void *ret = (void *)(hdr + 1);
|
||||
CHECK_LOW_ADDR(ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
void *runtime_low_malloc(size_t size) { return runtime_low_alloc(size); }
|
||||
|
||||
void *runtime_low_calloc(size_t nmemb, size_t size) {
|
||||
size_t total = nmemb * size;
|
||||
void *p = runtime_low_alloc(total);
|
||||
if (p) memset(p, 0, total);
|
||||
return p;
|
||||
}
|
||||
|
||||
void runtime_low_free(void *ptr) {
|
||||
if (!ptr) return;
|
||||
LowHeader *hdr = (LowHeader *)ptr - 1;
|
||||
if (hdr->magic != 0x4C4F5741) return;
|
||||
if (hdr->size_class < NUM_LOW_CLASSES) {
|
||||
host_mutex_lock(&low_alloc_lock);
|
||||
LowFreeNode *node = (LowFreeNode *)hdr;
|
||||
node->next = low_freelists[hdr->size_class];
|
||||
low_freelists[hdr->size_class] = node;
|
||||
host_mutex_unlock(&low_alloc_lock);
|
||||
}
|
||||
}
|
||||
|
||||
void *runtime_low_alloc_aligned(size_t size, size_t align) {
|
||||
if (align < 16) align = 16;
|
||||
size_t needed = size + align + sizeof(void *);
|
||||
void *raw = runtime_low_alloc(needed);
|
||||
if (!raw) return NULL;
|
||||
uintptr_t p = (uintptr_t)raw + sizeof(void *);
|
||||
p = (p + align - 1) & ~(align - 1);
|
||||
((void **)p)[-1] = raw;
|
||||
void *ret = (void *)p;
|
||||
CHECK_LOW_ADDR(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void runtime_low_free_aligned(void *ptr) {
|
||||
if (!ptr) return;
|
||||
void *raw = ((void **)ptr)[-1];
|
||||
runtime_low_free(raw);
|
||||
}
|
||||
/* ---- GPU library interface (gpu/shim/bbgpu.cpp) ---- */
|
||||
/* Optimizations switched off at run time (diagnostics): the number in the file named by
|
||||
@@ -523,9 +749,7 @@ void *runtime_low_map(size_t size, int prot) {
|
||||
* 1073741824 the lock-free UpdateImage path for clean, tracked images. */
|
||||
/* Bits 32 and up: the draw pipeline and related GPU thread work (gpu/shim/bbport_toggles.h). */
|
||||
uint64_t runtime_disabled_optimizations;
|
||||
/* Speculative readers of guest memory (GPU draw-preparation workers) register a recovery
|
||||
* point: a fault on that thread jumps back to it instead of terminating (probe.c). */
|
||||
__thread sigjmp_buf *runtime_fault_recover;
|
||||
__thread RuntimeRecoverBuf *runtime_fault_recover;
|
||||
static void *toggle_watcher(void *path) {
|
||||
for (unsigned long long last=ULLONG_MAX;;) {
|
||||
FILE *f=fopen(path,"r");
|
||||
@@ -550,6 +774,22 @@ void runtime_memory_set_gpu_hooks(GpuRange map, GpuRange unmap, GpuRange invalid
|
||||
write_unlock();
|
||||
flush_hooks();
|
||||
}
|
||||
/* Guest-visible writes from the GPU side (fence labels, WriteData, downloads): every byte is
|
||||
* stored once. glibc's memcpy copies 4-7 bytes as two overlapping 4-byte stores (the same
|
||||
* 4 bytes twice for a 4-byte label): a thread preempted between them stored the label again
|
||||
* after the guest had seen it and freed the block that held it (a free-list link ending in
|
||||
* 0x00000004: guest fault 0x263b8e7). Aligned 2, 4 and 8-byte pieces are single stores. */
|
||||
static void store_once(unsigned char *dst, const unsigned char *src, uint64_t n) {
|
||||
if (n > 64) { memcpy(dst,src,n); return; }
|
||||
while (n) {
|
||||
const uintptr_t at=(uintptr_t)dst;
|
||||
if (n>=8 && !(at&7)) { uint64_t v; memcpy(&v,src,8); __atomic_store_n((uint64_t *)dst,v,__ATOMIC_RELEASE); dst+=8; src+=8; n-=8; }
|
||||
else if (n>=4 && !(at&3)) { uint32_t v; memcpy(&v,src,4); __atomic_store_n((uint32_t *)dst,v,__ATOMIC_RELEASE); dst+=4; src+=4; n-=4; }
|
||||
else if (n>=2 && !(at&1)) { uint16_t v; memcpy(&v,src,2); __atomic_store_n((uint16_t *)dst,v,__ATOMIC_RELEASE); dst+=2; src+=2; n-=2; }
|
||||
else { __atomic_store_n(dst,*src,__ATOMIC_RELEASE); ++dst; ++src; --n; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Writes through the backing view; 0 when part of the range has no backing. */
|
||||
int runtime_memory_write_backing(uintptr_t address, const void *data, uint64_t size) {
|
||||
read_lock();
|
||||
@@ -558,7 +798,7 @@ int runtime_memory_write_backing(uintptr_t address, const void *data, uint64_t s
|
||||
size_t i=vma_index(at);
|
||||
if (i==vma_count || vmas[i].start>at || vmas[i].kind==KIND_RESERVED) { ok=0; break; }
|
||||
uint64_t n=(vmas[i].end<end ? vmas[i].end : end)-at;
|
||||
memcpy(backing_base+vmas[i].phys+(at-vmas[i].start),(const unsigned char *)data+(at-address),n);
|
||||
store_once(backing_base+vmas[i].phys+(at-vmas[i].start),(const unsigned char *)data+(at-address),n);
|
||||
at+=n;
|
||||
}
|
||||
read_unlock();
|
||||
@@ -598,6 +838,20 @@ uint64_t runtime_memory_clamp(uintptr_t address, uint64_t size) {
|
||||
read_unlock();
|
||||
return length<size ? length : size;
|
||||
}
|
||||
/* bbport: protection and direct memory type of the mapping at address (type -1: not direct
|
||||
* memory); 0 when nothing is mapped there. */
|
||||
int runtime_memory_vma_info(uintptr_t address, int *prot, int *type, uintptr_t *end) {
|
||||
read_lock();
|
||||
const size_t i = vma_index(address);
|
||||
const int ok = i < vma_count && vmas[i].start <= address && vmas[i].kind != KIND_RESERVED;
|
||||
if (ok) {
|
||||
if (prot) *prot = vmas[i].prot;
|
||||
if (type) *type = vmas[i].kind == KIND_DIRECT ? vmas[i].type : -1;
|
||||
if (end) *end = vmas[i].end;
|
||||
}
|
||||
read_unlock();
|
||||
return ok;
|
||||
}
|
||||
/* Region around address: [start,end) mapped or a gap; 0 past the last mapping. */
|
||||
int runtime_memory_region(uintptr_t address, uintptr_t *start, uintptr_t *end, int *mapped) {
|
||||
const CachedRegion *c=(runtime_disabled_optimizations & 1) ? NULL : cached_region(address);
|
||||
@@ -621,9 +875,24 @@ void runtime_memory_gpu_protect(uintptr_t address, uint64_t size, int read, int
|
||||
if (vmas[i].kind==KIND_RESERVED) continue;
|
||||
uintptr_t a=vmas[i].start>address ? vmas[i].start : address;
|
||||
uintptr_t b=vmas[i].end<address+size ? vmas[i].end : address+size;
|
||||
#ifdef _WIN32
|
||||
int guest=vmas[i].prot;
|
||||
int r = read && (guest & 0x11);
|
||||
int w = write && (guest & 0x22);
|
||||
int x = (guest & 4);
|
||||
DWORD p = PAGE_NOACCESS;
|
||||
if (x) {
|
||||
p = w ? PAGE_EXECUTE_READWRITE : (r ? PAGE_EXECUTE_READ : PAGE_EXECUTE);
|
||||
} else {
|
||||
p = w ? PAGE_READWRITE : (r ? PAGE_READONLY : PAGE_NOACCESS);
|
||||
}
|
||||
DWORD old;
|
||||
VirtualProtect((void *)a, b-a, p, &old);
|
||||
#else
|
||||
int guest=host_prot(vmas[i].prot);
|
||||
int want=(read ? PROT_READ : 0)|(write ? PROT_WRITE|PROT_READ : 0)|(guest & PROT_EXEC);
|
||||
mprotect((void *)a,b-a,guest & want);
|
||||
#endif
|
||||
}
|
||||
read_unlock();
|
||||
}
|
||||
@@ -642,9 +911,3 @@ void runtime_memory_report(void) {
|
||||
printf("Runtime: flexible maps=%zu, in use=%" PRIu64 ", protects=%zu, queries=%zu, regions=%zu\n",
|
||||
flexible_maps, flexible_bytes, protects, queries, vma_count);
|
||||
}
|
||||
#else
|
||||
void *runtime_low_map(size_t size, int prot) { (void)size; (void)prot; return NULL; }
|
||||
uintptr_t runtime_memory_resolve(const char *name) { (void)name; return 0; }
|
||||
int runtime_memory_is_mapped(uintptr_t address, uint64_t size) { (void)address; (void)size; return 0; }
|
||||
void runtime_memory_report(void) { puts("Runtime: Windows direct-memory backend not implemented"); }
|
||||
#endif
|
||||
+188
-77
@@ -1,15 +1,120 @@
|
||||
/* Linux pthread-backed opaque PS4 mutex handles. No guest threads yet. */
|
||||
/* Opaque PS4 mutex and condition variable handles over the host's own primitives: pthread
|
||||
* mutexes/condition variables on Linux; on Windows an SRW lock with owner and depth (the PS4
|
||||
* error-checking, recursive and normal types) and a condition variable. */
|
||||
#define _GNU_SOURCE
|
||||
#include "runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
enum { TYPE_ERRORCHECK = 1, TYPE_RECURSIVE = 2, TYPE_NORMAL = 3 }; /* 4: adaptive, error-checking here */
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef struct { SRWLOCK lock; DWORD owner; unsigned depth; int type; } NativeMutex;
|
||||
typedef CONDITION_VARIABLE NativeCond;
|
||||
static int native_init(NativeMutex *m, int type) {
|
||||
InitializeSRWLock(&m->lock); m->owner = 0; m->depth = 0; m->type = type; return 0;
|
||||
}
|
||||
static int native_destroy(NativeMutex *m) { return __atomic_load_n(&m->owner, __ATOMIC_RELAXED) ? EBUSY : 0; }
|
||||
static DWORD owner(const NativeMutex *m) { return __atomic_load_n(&m->owner, __ATOMIC_RELAXED); }
|
||||
static void take(NativeMutex *m, DWORD self) { __atomic_store_n(&m->owner, self, __ATOMIC_RELAXED); m->depth = 1; }
|
||||
/* Relocking by the owner: recursion, an error, or (normal type) a deadlock as on POSIX. */
|
||||
static int relock(NativeMutex *m) {
|
||||
if (m->type == TYPE_RECURSIVE) return m->depth == ~0u ? EAGAIN : (++m->depth, 0);
|
||||
return m->type == TYPE_NORMAL ? -1 : EDEADLK;
|
||||
}
|
||||
static int native_lock(NativeMutex *m) {
|
||||
DWORD self = GetCurrentThreadId();
|
||||
if (owner(m) == self) { int e = relock(m); if (e >= 0) return e; }
|
||||
AcquireSRWLockExclusive(&m->lock);
|
||||
take(m, self);
|
||||
return 0;
|
||||
}
|
||||
static int native_trylock(NativeMutex *m) {
|
||||
DWORD self = GetCurrentThreadId();
|
||||
if (owner(m) == self) return m->type == TYPE_RECURSIVE ? relock(m) : EBUSY;
|
||||
if (!TryAcquireSRWLockExclusive(&m->lock)) return EBUSY;
|
||||
take(m, self);
|
||||
return 0;
|
||||
}
|
||||
/* SRW locks have no timed acquire: retry until the realtime deadline (uncontended in practice). */
|
||||
static int native_timedlock(NativeMutex *m, uint64_t deadline) {
|
||||
DWORD self = GetCurrentThreadId();
|
||||
if (owner(m) == self) { int e = relock(m); if (e >= 0) return e; }
|
||||
for (unsigned attempt = 0;; ++attempt) {
|
||||
if (TryAcquireSRWLockExclusive(&m->lock)) { take(m, self); return 0; }
|
||||
if (host_realtime_ns() >= deadline) return ETIMEDOUT;
|
||||
if (attempt < 64) YieldProcessor(); else if (attempt < 128) SwitchToThread(); else Sleep(1);
|
||||
}
|
||||
}
|
||||
static int native_unlock(NativeMutex *m) {
|
||||
DWORD self = GetCurrentThreadId(), held = owner(m);
|
||||
/* A normal mutex may be unlocked by another thread (glibc does not check its owner). */
|
||||
if (held != self && (m->type != TYPE_NORMAL || !held)) return EPERM;
|
||||
if (held == self && --m->depth) return 0;
|
||||
__atomic_store_n(&m->owner, 0, __ATOMIC_RELAXED); m->depth = 0;
|
||||
ReleaseSRWLockExclusive(&m->lock);
|
||||
return 0;
|
||||
}
|
||||
static int native_cond_init(NativeCond *c) { InitializeConditionVariable(c); return 0; }
|
||||
static int native_cond_destroy(NativeCond *c) { (void)c; return 0; }
|
||||
/* deadline: realtime nanoseconds, 0 for none. The wait gives up the whole recursion. */
|
||||
static int native_cond_wait(NativeCond *c, NativeMutex *m, uint64_t deadline) {
|
||||
DWORD self = GetCurrentThreadId(), ms = INFINITE;
|
||||
if (owner(m) != self) return EPERM;
|
||||
if (deadline) {
|
||||
uint64_t now = host_realtime_ns();
|
||||
if (now >= deadline) return ETIMEDOUT;
|
||||
uint64_t left = (deadline - now + 999999) / 1000000;
|
||||
ms = left >= INFINITE ? INFINITE - 1 : (DWORD)left;
|
||||
}
|
||||
unsigned depth = m->depth;
|
||||
__atomic_store_n(&m->owner, 0, __ATOMIC_RELAXED); m->depth = 0;
|
||||
BOOL woken = SleepConditionVariableSRW(c, &m->lock, ms, 0);
|
||||
DWORD error = woken ? 0 : GetLastError();
|
||||
__atomic_store_n(&m->owner, self, __ATOMIC_RELAXED); m->depth = depth;
|
||||
return woken ? 0 : error == ERROR_TIMEOUT ? ETIMEDOUT : EINVAL;
|
||||
}
|
||||
static int native_cond_signal(NativeCond *c) { WakeConditionVariable(c); return 0; }
|
||||
static int native_cond_broadcast(NativeCond *c) { WakeAllConditionVariable(c); return 0; }
|
||||
#else
|
||||
#include <pthread.h>
|
||||
typedef pthread_mutex_t NativeMutex;
|
||||
typedef pthread_cond_t NativeCond;
|
||||
static int native_init(NativeMutex *m, int type) {
|
||||
pthread_mutexattr_t attr;
|
||||
int e = pthread_mutexattr_init(&attr);
|
||||
if (e) return e;
|
||||
int native_type = type == TYPE_RECURSIVE ? PTHREAD_MUTEX_RECURSIVE : type == TYPE_NORMAL ? PTHREAD_MUTEX_NORMAL : PTHREAD_MUTEX_ERRORCHECK;
|
||||
e = pthread_mutexattr_settype(&attr, native_type);
|
||||
if (!e) e = pthread_mutex_init(m, &attr);
|
||||
pthread_mutexattr_destroy(&attr);
|
||||
return e;
|
||||
}
|
||||
static int native_destroy(NativeMutex *m) { return pthread_mutex_destroy(m); }
|
||||
static int native_lock(NativeMutex *m) { return pthread_mutex_lock(m); }
|
||||
static int native_trylock(NativeMutex *m) { return pthread_mutex_trylock(m); }
|
||||
static struct timespec realtime(uint64_t ns) { return (struct timespec){(time_t)(ns / 1000000000), (long)(ns % 1000000000)}; }
|
||||
static int native_timedlock(NativeMutex *m, uint64_t deadline) {
|
||||
struct timespec end = realtime(deadline);
|
||||
return pthread_mutex_timedlock(m, &end);
|
||||
}
|
||||
static int native_unlock(NativeMutex *m) { return pthread_mutex_unlock(m); }
|
||||
static int native_cond_init(NativeCond *c) { return pthread_cond_init(c, NULL); }
|
||||
static int native_cond_destroy(NativeCond *c) { return pthread_cond_destroy(c); }
|
||||
static int native_cond_wait(NativeCond *c, NativeMutex *m, uint64_t deadline) {
|
||||
if (!deadline) return pthread_cond_wait(c, m);
|
||||
struct timespec end = realtime(deadline);
|
||||
return pthread_cond_timedwait(c, m, &end);
|
||||
}
|
||||
static int native_cond_signal(NativeCond *c) { return pthread_cond_signal(c); }
|
||||
static int native_cond_broadcast(NativeCond *c) { return pthread_cond_broadcast(c); }
|
||||
#endif
|
||||
|
||||
typedef struct { int type; } GuestAttr;
|
||||
typedef struct { pthread_mutex_t native; } GuestMutex;
|
||||
typedef struct { NativeMutex native; } GuestMutex;
|
||||
static size_t created, locks, unlocks;
|
||||
static int32_t orbis_error(int e) {
|
||||
if (!e) return 0;
|
||||
@@ -27,133 +132,126 @@ static int32_t orbis_error(int e) {
|
||||
}
|
||||
static ABI int32_t attr_init(GuestAttr **out) {
|
||||
if (!out) return orbis_error(EINVAL);
|
||||
GuestAttr *attr = malloc(sizeof(*attr));
|
||||
GuestAttr *attr = runtime_low_malloc(sizeof(*attr));
|
||||
if (!attr) return orbis_error(ENOMEM);
|
||||
CHECK_LOW_ADDR(attr);
|
||||
attr->type = 1; *out = attr; return 0;
|
||||
}
|
||||
static ABI int32_t attr_type(GuestAttr **attr, int type) {
|
||||
if (!attr || !*attr || type < 1 || type > 4) return orbis_error(EINVAL);
|
||||
(*attr)->type = type; return 0;
|
||||
}
|
||||
/* Priority protocols need a PS4 scheduler; the value is validated and recorded only. */
|
||||
static ABI int32_t attr_protocol(GuestAttr **attr, int protocol) {
|
||||
if (!attr || !*attr || protocol < 0 || protocol > 2) return orbis_error(EINVAL);
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t attr_destroy(GuestAttr **attr) {
|
||||
if (!attr || !*attr) return orbis_error(EINVAL);
|
||||
free(*attr); *attr = NULL; return 0;
|
||||
runtime_low_free(*attr); *attr = NULL; return 0;
|
||||
}
|
||||
static ABI int32_t mutex_init(GuestMutex **out, GuestAttr **attr, const char *name) {
|
||||
(void)name;
|
||||
if (!out || (attr && !*attr)) return orbis_error(EINVAL);
|
||||
int type = attr ? (*attr)->type : 1;
|
||||
if (type < 1 || type > 4) return orbis_error(EINVAL);
|
||||
GuestMutex *mutex = malloc(sizeof(*mutex));
|
||||
GuestMutex *mutex = runtime_low_malloc(sizeof(*mutex));
|
||||
if (!mutex) return orbis_error(ENOMEM);
|
||||
pthread_mutexattr_t native_attr;
|
||||
int e = pthread_mutexattr_init(&native_attr);
|
||||
if (e) { free(mutex); return orbis_error(e); }
|
||||
int native_type = type == 2 ? PTHREAD_MUTEX_RECURSIVE : type == 3 ? PTHREAD_MUTEX_NORMAL : PTHREAD_MUTEX_ERRORCHECK;
|
||||
e = pthread_mutexattr_settype(&native_attr, native_type);
|
||||
if (!e) e = pthread_mutex_init(&mutex->native, &native_attr);
|
||||
pthread_mutexattr_destroy(&native_attr);
|
||||
if (e) { free(mutex); return orbis_error(e); }
|
||||
CHECK_LOW_ADDR(mutex);
|
||||
int e = native_init(&mutex->native, type);
|
||||
if (e) { runtime_low_free(mutex); return orbis_error(e); }
|
||||
*out = mutex; ++created; return 0;
|
||||
}
|
||||
static pthread_mutex_t static_init = PTHREAD_MUTEX_INITIALIZER;
|
||||
/* Statically initialized handles (0/1) are created once, even under contention. */
|
||||
static HostMutex static_init = HOST_MUTEX_INIT;
|
||||
static int32_t ensure_mutex(GuestMutex **mutex) {
|
||||
if (!mutex) return orbis_error(EINVAL);
|
||||
uintptr_t value = __atomic_load_n((uintptr_t *)mutex, __ATOMIC_ACQUIRE);
|
||||
if (value == 2) return orbis_error(EINVAL);
|
||||
if (value >= 2) return 0;
|
||||
pthread_mutex_lock(&static_init);
|
||||
host_mutex_lock(&static_init);
|
||||
int32_t e = 0;
|
||||
if ((uintptr_t)*mutex < 2) {
|
||||
GuestMutex *created_mutex = NULL;
|
||||
e = mutex_init(&created_mutex, NULL, NULL);
|
||||
if (!e) __atomic_store_n(mutex, created_mutex, __ATOMIC_RELEASE);
|
||||
}
|
||||
pthread_mutex_unlock(&static_init);
|
||||
host_mutex_unlock(&static_init);
|
||||
return e;
|
||||
}
|
||||
static ABI int32_t mutex_lock(GuestMutex **mutex) {
|
||||
int32_t e = ensure_mutex(mutex);
|
||||
if (e) return e;
|
||||
e = orbis_error(pthread_mutex_lock(&(*mutex)->native));
|
||||
runtime_thread_set_blocked("mutex", (uintptr_t)mutex);
|
||||
e = orbis_error(native_lock(&(*mutex)->native));
|
||||
runtime_thread_clear_blocked();
|
||||
if (!e) ++locks;
|
||||
restore_guest_fs();
|
||||
return e;
|
||||
}
|
||||
static ABI int32_t mutex_trylock(GuestMutex **mutex) {
|
||||
int32_t e = ensure_mutex(mutex);
|
||||
if (e) return e;
|
||||
e = orbis_error(pthread_mutex_trylock(&(*mutex)->native));
|
||||
e = orbis_error(native_trylock(&(*mutex)->native));
|
||||
if (!e) ++locks;
|
||||
restore_guest_fs();
|
||||
return e;
|
||||
}
|
||||
static ABI int32_t mutex_unlock(GuestMutex **mutex) {
|
||||
if (!mutex || (uintptr_t)*mutex == 2) return orbis_error(EINVAL);
|
||||
if ((uintptr_t)*mutex < 2) return orbis_error(EPERM);
|
||||
int32_t e = orbis_error(pthread_mutex_unlock(&(*mutex)->native));
|
||||
int32_t e = orbis_error(native_unlock(&(*mutex)->native));
|
||||
if (!e) ++unlocks;
|
||||
restore_guest_fs();
|
||||
return e;
|
||||
}
|
||||
static ABI int32_t mutex_destroy(GuestMutex **mutex) {
|
||||
if (!mutex || (uintptr_t)*mutex == 2) return orbis_error(EINVAL);
|
||||
if ((uintptr_t)*mutex < 2) return 0;
|
||||
int e = pthread_mutex_destroy(&(*mutex)->native);
|
||||
if (!e) { free(*mutex); *mutex = (GuestMutex *)(uintptr_t)2; }
|
||||
int e = native_destroy(&(*mutex)->native);
|
||||
if (!e) { runtime_low_free(*mutex); *mutex = (GuestMutex *)(uintptr_t)2; }
|
||||
return orbis_error(e);
|
||||
}
|
||||
static int deadline_after(struct timespec *end, uint64_t usec) {
|
||||
if (clock_gettime(CLOCK_REALTIME, end)) return errno;
|
||||
uint64_t ns = (uint64_t)end->tv_nsec + (usec % 1000000) * 1000;
|
||||
end->tv_sec += (time_t)(usec / 1000000 + ns / 1000000000);
|
||||
end->tv_nsec = (long)(ns % 1000000000);
|
||||
return 0;
|
||||
}
|
||||
static uint64_t deadline_after(uint64_t usec) { return host_realtime_ns() + usec * 1000; }
|
||||
static int32_t timed_error(int e) { return e == ETIMEDOUT ? (int32_t)UINT32_C(0x8002003c) : orbis_error(e); }
|
||||
static ABI int32_t mutex_timedlock(GuestMutex **mutex, uint32_t usec) {
|
||||
int32_t e = ensure_mutex(mutex);
|
||||
if (e) return e;
|
||||
struct timespec end;
|
||||
if (deadline_after(&end, usec)) return orbis_error(EINVAL);
|
||||
e = timed_error(pthread_mutex_timedlock(&(*mutex)->native, &end));
|
||||
runtime_thread_set_blocked("mutex_timed", (uintptr_t)mutex);
|
||||
e = timed_error(native_timedlock(&(*mutex)->native, deadline_after(usec)));
|
||||
runtime_thread_clear_blocked();
|
||||
if (!e) ++locks;
|
||||
restore_guest_fs();
|
||||
return e;
|
||||
}
|
||||
/* Condition variables: opaque handles over host pthread_cond (CLOCK_REALTIME,
|
||||
* like FreeBSD's default), sharing the host mutex behind GuestMutex. */
|
||||
typedef struct { pthread_cond_t native; } GuestCond;
|
||||
typedef struct { NativeCond native; } GuestCond;
|
||||
static size_t conds, waits, wakeups;
|
||||
static ABI int32_t cond_init(GuestCond **out, void **attr, const char *name) {
|
||||
(void)attr; (void)name;
|
||||
if (!out) return orbis_error(EINVAL);
|
||||
GuestCond *c = malloc(sizeof(*c));
|
||||
GuestCond *c = runtime_low_malloc(sizeof(*c));
|
||||
if (!c) return orbis_error(ENOMEM);
|
||||
int e = pthread_cond_init(&c->native, NULL);
|
||||
if (e) { free(c); return orbis_error(e); }
|
||||
CHECK_LOW_ADDR(c);
|
||||
int e = native_cond_init(&c->native);
|
||||
if (e) { runtime_low_free(c); return orbis_error(e); }
|
||||
*out = c; ++conds; return 0;
|
||||
}
|
||||
static int32_t ensure_cond(GuestCond **cond) {
|
||||
if (!cond) return orbis_error(EINVAL);
|
||||
if (__atomic_load_n((uintptr_t *)cond, __ATOMIC_ACQUIRE) >= 2) return 0;
|
||||
pthread_mutex_lock(&static_init);
|
||||
host_mutex_lock(&static_init);
|
||||
int32_t e = 0;
|
||||
if ((uintptr_t)*cond < 2) {
|
||||
GuestCond *c = NULL;
|
||||
e = cond_init(&c, NULL, NULL);
|
||||
if (!e) __atomic_store_n(cond, c, __ATOMIC_RELEASE);
|
||||
}
|
||||
pthread_mutex_unlock(&static_init);
|
||||
host_mutex_unlock(&static_init);
|
||||
return e;
|
||||
}
|
||||
static ABI int32_t cond_destroy(GuestCond **cond) {
|
||||
if (!cond) return orbis_error(EINVAL);
|
||||
if ((uintptr_t)*cond < 2) return 0;
|
||||
int e = pthread_cond_destroy(&(*cond)->native);
|
||||
if (!e) { free(*cond); *cond = NULL; }
|
||||
int e = native_cond_destroy(&(*cond)->native);
|
||||
if (!e) { runtime_low_free(*cond); *cond = NULL; }
|
||||
return orbis_error(e);
|
||||
}
|
||||
static ABI int32_t cond_wait(GuestCond **cond, GuestMutex **mutex) {
|
||||
@@ -161,36 +259,42 @@ static ABI int32_t cond_wait(GuestCond **cond, GuestMutex **mutex) {
|
||||
if (e) return e;
|
||||
if (!mutex || (uintptr_t)*mutex < 3) return orbis_error(EINVAL);
|
||||
++waits;
|
||||
return orbis_error(pthread_cond_wait(&(*cond)->native, &(*mutex)->native));
|
||||
runtime_thread_set_blocked("condvar", (uintptr_t)cond);
|
||||
int32_t res = orbis_error(native_cond_wait(&(*cond)->native, &(*mutex)->native, 0));
|
||||
runtime_thread_clear_blocked();
|
||||
restore_guest_fs();
|
||||
return res;
|
||||
}
|
||||
static int32_t cond_wait_until(GuestCond **cond, GuestMutex **mutex, const struct timespec *end) {
|
||||
static int32_t cond_wait_until(GuestCond **cond, GuestMutex **mutex, uint64_t deadline) {
|
||||
int32_t e = ensure_cond(cond);
|
||||
if (e) return e;
|
||||
if (!mutex || (uintptr_t)*mutex < 3 || !end) return orbis_error(EINVAL);
|
||||
if (!mutex || (uintptr_t)*mutex < 3) return orbis_error(EINVAL);
|
||||
++waits;
|
||||
return timed_error(pthread_cond_timedwait(&(*cond)->native, &(*mutex)->native, end));
|
||||
runtime_thread_set_blocked("condvar_timed", (uintptr_t)cond);
|
||||
int32_t res = timed_error(native_cond_wait(&(*cond)->native, &(*mutex)->native, deadline ? deadline : 1));
|
||||
runtime_thread_clear_blocked();
|
||||
restore_guest_fs();
|
||||
return res;
|
||||
}
|
||||
static ABI int32_t cond_timedwait(GuestCond **cond, GuestMutex **mutex, uint32_t usec) {
|
||||
struct timespec end;
|
||||
if (deadline_after(&end, usec)) return orbis_error(EINVAL);
|
||||
return cond_wait_until(cond, mutex, &end);
|
||||
return cond_wait_until(cond, mutex, deadline_after(usec));
|
||||
}
|
||||
static ABI int32_t cond_signal(GuestCond **cond) {
|
||||
int32_t e = ensure_cond(cond);
|
||||
if (e) return e;
|
||||
++wakeups; return orbis_error(pthread_cond_signal(&(*cond)->native));
|
||||
++wakeups; return orbis_error(native_cond_signal(&(*cond)->native));
|
||||
}
|
||||
static ABI int32_t cond_broadcast(GuestCond **cond) {
|
||||
int32_t e = ensure_cond(cond);
|
||||
if (e) return e;
|
||||
++wakeups; return orbis_error(pthread_cond_broadcast(&(*cond)->native));
|
||||
++wakeups; return orbis_error(native_cond_broadcast(&(*cond)->native));
|
||||
}
|
||||
/* PS4 struct timespec is {int64 sec, int64 nsec}, identical to Linux x86-64. */
|
||||
typedef struct { int64_t sec, nsec; } GuestTimespec;
|
||||
static ABI int32_t cond_abs_timedwait(GuestCond **cond, GuestMutex **mutex, const GuestTimespec *abs) {
|
||||
if (!abs || abs->nsec < 0 || abs->nsec >= 1000000000) return orbis_error(EINVAL);
|
||||
struct timespec end = {.tv_sec = (time_t)abs->sec, .tv_nsec = (long)abs->nsec};
|
||||
return cond_wait_until(cond, mutex, &end);
|
||||
uint64_t deadline = (uint64_t)abs->sec * 1000000000ULL + (uint64_t)abs->nsec;
|
||||
return cond_wait_until(cond, mutex, deadline);
|
||||
}
|
||||
/* POSIX pthread APIs return positive guest errno values, unlike scePthread. */
|
||||
static int32_t posix_result(int32_t result) { return result ? (int32_t)((uint32_t)result & 0xffff) : 0; }
|
||||
@@ -245,26 +349,33 @@ void runtime_mutex_report(void) {
|
||||
printf("Runtime: mutexes created=%zu, locks=%zu, unlocks=%zu\n", created, locks, unlocks);
|
||||
printf("Runtime: condition variables created=%zu, waits=%zu, wakeups=%zu\n", conds, waits, wakeups);
|
||||
}
|
||||
void runtime_mutex_dump_info(void *f_ptr, void *handle) {
|
||||
FILE *f = (FILE *)f_ptr;
|
||||
if (!handle || !f) return;
|
||||
GuestMutex **mptr = (GuestMutex **)handle;
|
||||
GuestMutex *m = NULL;
|
||||
#ifdef _WIN32
|
||||
if (!IsBadReadPtr(mptr, sizeof(void *))) {
|
||||
m = *mptr;
|
||||
} else {
|
||||
m = (GuestMutex *)handle;
|
||||
}
|
||||
if (!m || (uintptr_t)m < 3 || IsBadReadPtr(m, sizeof(GuestMutex))) return;
|
||||
const char *owner_name = "none";
|
||||
DWORD owner_tid = m->native.owner;
|
||||
if (owner_tid) {
|
||||
for (GuestThread *t = runtime_thread_get_all(); t; t = t->next) {
|
||||
if (t->win32_tid == owner_tid) {
|
||||
owner_name = t->name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fprintf(f, " [mutex %p: depth=%u, owner='%s' (tid=%lu)]\n",
|
||||
(void *)m, m->native.depth, owner_name, (unsigned long)owner_tid);
|
||||
#else
|
||||
static ABI int32_t posix_cond_init(GuestCond **c, void **a) { return posix_result(cond_init(c, a, NULL)); }
|
||||
static ABI int32_t posix_cond_destroy(GuestCond **c) { return posix_result(cond_destroy(c)); }
|
||||
static ABI int32_t posix_cond_wait(GuestCond **c, GuestMutex **m) { return posix_result(cond_wait(c, m)); }
|
||||
static ABI int32_t posix_cond_timedwait(GuestCond **c, GuestMutex **m, const GuestTimespec *t) { return posix_result(cond_abs_timedwait(c, m, t)); }
|
||||
static ABI int32_t posix_cond_signal(GuestCond **c) { return posix_result(cond_signal(c)); }
|
||||
static ABI int32_t posix_cond_broadcast(GuestCond **c) { return posix_result(cond_broadcast(c)); }
|
||||
uintptr_t runtime_mutex_resolve(const char *name) {
|
||||
if (!strcmp(name,"0TyVk4MSLt0#I#J")) return (uintptr_t)posix_cond_init;
|
||||
if (!strcmp(name,"RXXqi4CtF8w#I#J")) return (uintptr_t)posix_cond_destroy;
|
||||
if (!strcmp(name,"Op8TBGY5KHg#I#J")) return (uintptr_t)posix_cond_wait;
|
||||
if (!strcmp(name,"27bAgiJmOh0#I#J")) return (uintptr_t)posix_cond_timedwait;
|
||||
if (!strcmp(name,"2MOy+rUfuhQ#I#J")) return (uintptr_t)posix_cond_signal;
|
||||
if (!strcmp(name,"mkx2fVhNMsg#I#J")) return (uintptr_t)posix_cond_broadcast;
|
||||
if (!strcmp(name,"2Tb92quprl0#p#J")) return (uintptr_t)cond_init;
|
||||
if (!strcmp(name,"g+PZd2hiacg#p#J")) return (uintptr_t)cond_destroy;
|
||||
if (!strcmp(name,"WKAXJ4XBPQ4#p#J")) return (uintptr_t)cond_wait;
|
||||
if (!strcmp(name,"BmMjYxmew1w#p#J")) return (uintptr_t)cond_timedwait;
|
||||
if (!strcmp(name,"kDh-NfxgMtE#p#J")) return (uintptr_t)cond_signal;
|
||||
if (!strcmp(name,"JGgj7Uvrl+A#p#J")) return (uintptr_t)cond_broadcast;
|
||||
if (!strcmp(name,"IafI2PxcPnQ#p#J")) return (uintptr_t)mutex_timedlock; (void)name; return 0; }
|
||||
void runtime_mutex_report(void) { puts("Runtime: Windows mutex backend not implemented"); }
|
||||
if (mptr) m = *mptr; else m = (GuestMutex *)handle;
|
||||
if (!m || (uintptr_t)m < 3) return;
|
||||
fprintf(f, " [mutex %p]\n", (void *)m);
|
||||
#endif
|
||||
}
|
||||
+38
-17
@@ -16,6 +16,9 @@
|
||||
#include <time.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#define ERR_INVALID_ARG ((int32_t)0x80920001)
|
||||
#define ERR_INVALID_HANDLE ((int32_t)0x80920003)
|
||||
@@ -55,13 +58,13 @@ _Static_assert(__builtin_offsetof(PadData,touches)==60,"OrbisPadData touch offse
|
||||
_Static_assert(__builtin_offsetof(PadData,timestamp)==80,"OrbisPadData timestamp offset");
|
||||
_Static_assert(sizeof(ControllerInfo)==28,"OrbisPadControllerInformation layout");
|
||||
|
||||
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
|
||||
static HostMutex lock = HOST_MUTEX_INIT;
|
||||
static int initialized, opened, sdl_ready;
|
||||
static SDL_Gamepad *gamepad;
|
||||
static size_t reads;
|
||||
static uint8_t connected_count;
|
||||
|
||||
static uint64_t now_us(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return (uint64_t)t.tv_sec*1000000u+(uint64_t)t.tv_nsec/1000u; }
|
||||
static uint64_t now_us(void) { return host_monotonic_ns() / 1000u; }
|
||||
static uint8_t axis(int16_t v) { int x=(v+32768)>>8; return (uint8_t)(x<0 ? 0 : x>255 ? 255 : x); }
|
||||
static uint8_t trigger(int16_t v) { int x=v>>7; return (uint8_t)(x<0 ? 0 : x>255 ? 255 : x); }
|
||||
static uint16_t touch_axis(float v, int max) {
|
||||
@@ -134,10 +137,15 @@ static void sample_host(PadData *d) {
|
||||
}
|
||||
if (!k) return;
|
||||
static const struct { SDL_Scancode key; uint32_t ps; } keys[]={
|
||||
{SDL_SCANCODE_SPACE,BTN_CROSS}, {SDL_SCANCODE_LSHIFT,BTN_CIRCLE}, {SDL_SCANCODE_E,BTN_SQUARE},
|
||||
{SDL_SCANCODE_Q,BTN_TRIANGLE}, {SDL_SCANCODE_1,BTN_L1}, {SDL_SCANCODE_3,BTN_R1},
|
||||
{SDL_SCANCODE_R,BTN_L2}, {SDL_SCANCODE_F,BTN_R2}, {SDL_SCANCODE_Z,BTN_L3}, {SDL_SCANCODE_C,BTN_R3},
|
||||
{SDL_SCANCODE_RETURN,BTN_OPTIONS},
|
||||
{SDL_SCANCODE_SPACE,BTN_CROSS}, {SDL_SCANCODE_RETURN,BTN_CROSS}, {SDL_SCANCODE_KP_ENTER,BTN_CROSS},
|
||||
{SDL_SCANCODE_LSHIFT,BTN_CIRCLE}, {SDL_SCANCODE_ESCAPE,BTN_CIRCLE},
|
||||
{SDL_SCANCODE_E,BTN_SQUARE}, {SDL_SCANCODE_Q,BTN_TRIANGLE},
|
||||
{SDL_SCANCODE_1,BTN_L1}, {SDL_SCANCODE_3,BTN_R1},
|
||||
{SDL_SCANCODE_R,BTN_L2}, {SDL_SCANCODE_F,BTN_R2},
|
||||
{SDL_SCANCODE_Z,BTN_L3}, {SDL_SCANCODE_C,BTN_R3},
|
||||
{SDL_SCANCODE_F1,BTN_OPTIONS}, {SDL_SCANCODE_O,BTN_OPTIONS},
|
||||
{SDL_SCANCODE_UP,BTN_UP}, {SDL_SCANCODE_DOWN,BTN_DOWN},
|
||||
{SDL_SCANCODE_LEFT,BTN_LEFT}, {SDL_SCANCODE_RIGHT,BTN_RIGHT},
|
||||
{SDL_SCANCODE_I,BTN_UP}, {SDL_SCANCODE_K,BTN_DOWN}, {SDL_SCANCODE_J,BTN_LEFT}, {SDL_SCANCODE_L,BTN_RIGHT},
|
||||
};
|
||||
for (size_t i=0;i<sizeof(keys)/sizeof(*keys);++i) if (k[keys[i].key]) d->buttons|=keys[i].ps;
|
||||
@@ -160,16 +168,29 @@ static struct { uint32_t buttons; int stick[4]; int touch_side; } injected={0,{-
|
||||
static int replay_armed; /* 1 while a BB_PAD_REPLAY recording plays, 2 once it ended */
|
||||
static uint64_t replay_start; /* 0: (re)start at the next sample */
|
||||
static void read_inject(void) {
|
||||
static const char *path; static int checked; static uint64_t last_check; static struct timespec mtime;
|
||||
static const char *path; static int checked; static uint64_t last_check;
|
||||
#ifndef _WIN32
|
||||
static struct timespec mtime;
|
||||
#endif
|
||||
if (!checked) { path=getenv("BB_PAD_FILE"); checked=1; }
|
||||
if (!path || !*path) return;
|
||||
uint64_t now=now_us();
|
||||
if (now-last_check<20000) return;
|
||||
last_check=now;
|
||||
struct stat st;
|
||||
#ifdef _WIN32
|
||||
static FILETIME last_ft;
|
||||
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (hFile == INVALID_HANDLE_VALUE) return;
|
||||
FILETIME ft;
|
||||
GetFileTime(hFile, NULL, NULL, &ft);
|
||||
CloseHandle(hFile);
|
||||
if (ft.dwLowDateTime == last_ft.dwLowDateTime && ft.dwHighDateTime == last_ft.dwHighDateTime) return;
|
||||
last_ft = ft;
|
||||
#else
|
||||
if (stat(path,&st)!=0) return;
|
||||
if (st.st_mtim.tv_sec==mtime.tv_sec && st.st_mtim.tv_nsec==mtime.tv_nsec) return;
|
||||
mtime=st.st_mtim;
|
||||
#endif
|
||||
FILE *f=fopen(path,"r");
|
||||
if (!f) return;
|
||||
static const struct { const char *name; uint32_t ps; } names[]={
|
||||
@@ -279,16 +300,16 @@ static void sample(PadData *d) {
|
||||
for (int i=0;i<4;++i) if (injected.stick[i]>=0) *axes[i]=(uint8_t)injected.stick[i];
|
||||
}
|
||||
|
||||
static ABI int32_t pad_init(void) { pthread_mutex_lock(&lock); initialized=1; pthread_mutex_unlock(&lock); return 0; }
|
||||
static ABI int32_t pad_init(void) { host_lock(&lock); initialized=1; host_unlock(&lock); return 0; }
|
||||
static ABI int32_t pad_open(int32_t user, int32_t type, int32_t index, const void *param) {
|
||||
(void)param;
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (user!=1) return ERR_INVALID_ARG;
|
||||
if (type!=0 && type!=2) return ERR_INVALID_ARG; /* standard / special port */
|
||||
if (index) return ERR_INVALID_ARG;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
int already=opened; opened=1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
if (already) return ERR_ALREADY_OPENED;
|
||||
puts("Runtime: pad opened for user 1 (SDL gamepad or keyboard)");
|
||||
return PAD_HANDLE;
|
||||
@@ -300,9 +321,9 @@ static ABI int32_t pad_close(int32_t handle) {
|
||||
static ABI int32_t pad_read_state(int32_t handle, PadData *data) {
|
||||
if (handle!=PAD_HANDLE || !opened) return ERR_INVALID_HANDLE;
|
||||
if (!data) return ERR_INVALID_ARG;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
sample(data); ++reads;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
/* Buffered read: the port samples once per call, so one entry is returned. */
|
||||
@@ -319,19 +340,19 @@ static ABI int32_t pad_info(int32_t handle, ControllerInfo *info) {
|
||||
info->pixel_density=44.86f; info->resolution_x=1920; info->resolution_y=943;
|
||||
info->dead_zone_left=info->dead_zone_right=2;
|
||||
info->connection_type=0; info->connected=1; info->device_class=0;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
current_gamepad();
|
||||
info->connected_count=connected_count ? connected_count : 1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t pad_vibration(int32_t handle, const uint8_t *param) {
|
||||
if (handle!=PAD_HANDLE || !opened) return ERR_INVALID_HANDLE;
|
||||
if (!param) return ERR_INVALID_ARG;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
SDL_Gamepad *g=current_gamepad();
|
||||
if (g) SDL_RumbleGamepad(g,(uint16_t)(param[0]*257),(uint16_t)(param[1]*257),1000);
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t pad_ok_handle(int32_t handle) { return handle==PAD_HANDLE && opened ? 0 : ERR_INVALID_HANDLE; }
|
||||
|
||||
@@ -68,10 +68,17 @@ static uint64_t now_utc(void) {
|
||||
}
|
||||
/* Offset of local time from UTC at the given UTC tick, in microseconds. */
|
||||
static int64_t local_offset(uint64_t utc) {
|
||||
#ifdef _WIN32
|
||||
(void)utc;
|
||||
long tz = 0;
|
||||
_get_timezone(&tz);
|
||||
return -(int64_t)tz * 1000000;
|
||||
#else
|
||||
time_t seconds=(time_t)((int64_t)(utc-UNIX_EPOCH_TICKS)/1000000);
|
||||
struct tm local;
|
||||
localtime_r(&seconds,&local);
|
||||
return (int64_t)local.tm_gmtoff*1000000;
|
||||
#endif
|
||||
}
|
||||
|
||||
static ABI int32_t rtc_current_local(DateTime *t) {
|
||||
|
||||
+138
-51
@@ -1,29 +1,72 @@
|
||||
/* Linux rwlocks behind PS4 pointer-to-handle ABI. The registry protects
|
||||
lifetime, static initialization and ownership; waiting happens outside it. */
|
||||
/* Host rwlocks (pthread rwlocks on Linux, SRW locks on Windows) behind the PS4
|
||||
pointer-to-handle ABI. The registry protects lifetime, static initialization and
|
||||
ownership; waiting happens outside it. */
|
||||
#define _GNU_SOURCE
|
||||
#include "runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
|
||||
typedef struct { int64_t seconds, nanoseconds; } GuestTime;
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef SRWLOCK NativeRwlock;
|
||||
static int native_init(NativeRwlock *l) { InitializeSRWLock(l); return 0; }
|
||||
static int native_destroy(NativeRwlock *l) { (void)l; return 0; }
|
||||
static int native_try(NativeRwlock *l, int writer) {
|
||||
return (writer ? TryAcquireSRWLockExclusive(l) : TryAcquireSRWLockShared(l)) ? 0 : EBUSY;
|
||||
}
|
||||
static int native_lock(NativeRwlock *l, int writer) {
|
||||
if (writer) AcquireSRWLockExclusive(l); else AcquireSRWLockShared(l);
|
||||
return 0;
|
||||
}
|
||||
/* SRW locks have no timed acquire: retry until the realtime deadline. */
|
||||
static int native_timedlock(NativeRwlock *l, int writer, const GuestTime *time) {
|
||||
uint64_t deadline = (uint64_t)time->seconds * 1000000000ull + (uint64_t)time->nanoseconds;
|
||||
for (unsigned attempt = 0; ; ++attempt) {
|
||||
if (!native_try(l, writer)) return 0;
|
||||
if (host_realtime_ns() >= deadline) return ETIMEDOUT;
|
||||
if (attempt < 64) YieldProcessor();
|
||||
else if (attempt < 128) SwitchToThread();
|
||||
else Sleep(1);
|
||||
}
|
||||
}
|
||||
static int native_unlock(NativeRwlock *l, int writer) {
|
||||
if (writer) ReleaseSRWLockExclusive(l); else ReleaseSRWLockShared(l);
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
typedef pthread_rwlock_t NativeRwlock;
|
||||
static int native_init(NativeRwlock *l) { return pthread_rwlock_init(l, NULL); }
|
||||
static int native_destroy(NativeRwlock *l) { return pthread_rwlock_destroy(l); }
|
||||
static int native_try(NativeRwlock *l, int writer) { return writer ? pthread_rwlock_trywrlock(l) : pthread_rwlock_tryrdlock(l); }
|
||||
static int native_lock(NativeRwlock *l, int writer) { return writer ? pthread_rwlock_wrlock(l) : pthread_rwlock_rdlock(l); }
|
||||
static int native_timedlock(NativeRwlock *l, int writer, const GuestTime *time) {
|
||||
struct timespec deadline = {.tv_sec = (time_t)time->seconds, .tv_nsec = (long)time->nanoseconds};
|
||||
return writer ? pthread_rwlock_timedwrlock(l, &deadline) : pthread_rwlock_timedrdlock(l, &deadline);
|
||||
}
|
||||
static int native_unlock(NativeRwlock *l, int writer) { (void)writer; return pthread_rwlock_unlock(l); }
|
||||
#endif
|
||||
|
||||
typedef struct Holder {
|
||||
pthread_t thread;
|
||||
HostThreadId thread;
|
||||
unsigned readers, writer, pending;
|
||||
struct Holder *next;
|
||||
} Holder;
|
||||
|
||||
typedef struct Rwlock {
|
||||
pthread_rwlock_t native;
|
||||
NativeRwlock native;
|
||||
Holder *holders;
|
||||
unsigned inflight;
|
||||
struct Rwlock *next;
|
||||
} Rwlock;
|
||||
typedef struct { int64_t seconds, nanoseconds; } GuestTime;
|
||||
static pthread_mutex_t registry_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static HostMutex registry_lock = HOST_MUTEX_INIT;
|
||||
static Rwlock *registry;
|
||||
static size_t created, reads, writes, unlocks;
|
||||
|
||||
static int32_t error(int e) {
|
||||
if (!e) return 0;
|
||||
unsigned code;
|
||||
@@ -39,110 +82,157 @@ static int32_t error(int e) {
|
||||
}
|
||||
return (int32_t)(UINT32_C(0x80020000)|code);
|
||||
}
|
||||
|
||||
/* All helpers ending in _locked require registry_lock. */
|
||||
static Rwlock *find_locked(Rwlock *handle) {
|
||||
for (Rwlock *r=registry; r; r=r->next) if (r==handle) return r;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int create_locked(Rwlock **out) {
|
||||
Rwlock *r=calloc(1,sizeof(*r));
|
||||
Rwlock *r=runtime_low_calloc(1,sizeof(*r));
|
||||
if (!r) return ENOMEM;
|
||||
int e=pthread_rwlock_init(&r->native,NULL);
|
||||
if (e) { free(r); return e; }
|
||||
CHECK_LOW_ADDR(r);
|
||||
int e=native_init(&r->native);
|
||||
if (e) { runtime_low_free(r); return e; }
|
||||
r->next=registry; registry=r; *out=r; ++created; return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t rw_init(Rwlock **out, void *const *attr, const char *name) {
|
||||
(void)name;
|
||||
if (!out || (attr && !*attr)) return error(EINVAL);
|
||||
if (attr) { fputs("STOP: non-default PS4 rwlock attributes are not implemented\n",stderr); exit(21); }
|
||||
pthread_mutex_lock(®istry_lock);
|
||||
host_lock(®istry_lock);
|
||||
/* POSIX allows uninitialized storage, so never read *out during init. */
|
||||
int e=create_locked(out);
|
||||
pthread_mutex_unlock(®istry_lock); return error(e);
|
||||
host_unlock(®istry_lock); return error(e);
|
||||
}
|
||||
|
||||
static void prune_locked(Rwlock *r, Holder *h) {
|
||||
if (h->readers || h->writer || h->pending) return;
|
||||
Holder **link=&r->holders;
|
||||
while (*link!=h) link=&(*link)->next;
|
||||
*link=h->next; free(h);
|
||||
*link=h->next; runtime_low_free(h);
|
||||
}
|
||||
|
||||
enum Operation { READ, WRITE, TRY_READ, TRY_WRITE, TIMED_READ, TIMED_WRITE };
|
||||
static int32_t acquire(Rwlock **handle, enum Operation op, const GuestTime *time) {
|
||||
if (!handle) return error(EINVAL);
|
||||
int writer=op==WRITE || op==TRY_WRITE || op==TIMED_WRITE;
|
||||
int attempt=op==TRY_READ || op==TRY_WRITE;
|
||||
int timed=op==TIMED_READ || op==TIMED_WRITE;
|
||||
pthread_mutex_lock(®istry_lock);
|
||||
host_lock(®istry_lock);
|
||||
int e=0;
|
||||
if (!*handle) e=create_locked(handle);
|
||||
Rwlock *r=e ? NULL : find_locked(*handle);
|
||||
if (!r) { pthread_mutex_unlock(®istry_lock); return error(e ? e : EINVAL); }
|
||||
if (!r) { host_unlock(®istry_lock); return error(e ? e : EINVAL); }
|
||||
Holder *h=r->holders;
|
||||
while (h && !pthread_equal(h->thread,pthread_self())) h=h->next;
|
||||
while (h && !host_thread_equal(h->thread,host_thread_id())) h=h->next;
|
||||
if (h && (h->writer || (writer && h->readers))) {
|
||||
pthread_mutex_unlock(®istry_lock); return error(attempt ? EBUSY : EDEADLK);
|
||||
host_unlock(®istry_lock); return error(attempt ? EBUSY : EDEADLK);
|
||||
}
|
||||
if (h && h->readers==UINT_MAX) { pthread_mutex_unlock(®istry_lock); return error(EAGAIN); }
|
||||
if (h && h->readers==UINT_MAX) { host_unlock(®istry_lock); return error(EAGAIN); }
|
||||
if (!h) {
|
||||
h=calloc(1,sizeof(*h));
|
||||
if (!h) { pthread_mutex_unlock(®istry_lock); return error(ENOMEM); }
|
||||
h->thread=pthread_self(); h->next=r->holders; r->holders=h;
|
||||
h=runtime_low_calloc(1,sizeof(*h));
|
||||
if (!h) { host_unlock(®istry_lock); return error(ENOMEM); }
|
||||
h->thread=host_thread_id(); h->next=r->holders; r->holders=h;
|
||||
}
|
||||
++r->inflight; ++h->pending;
|
||||
pthread_mutex_unlock(®istry_lock);
|
||||
host_unlock(®istry_lock);
|
||||
|
||||
/* Timed operations first try the lock, matching POSIX timeout validation. */
|
||||
if (attempt || timed) e=writer ? pthread_rwlock_trywrlock(&r->native) : pthread_rwlock_tryrdlock(&r->native);
|
||||
else e=writer ? pthread_rwlock_wrlock(&r->native) : pthread_rwlock_rdlock(&r->native);
|
||||
runtime_thread_set_blocked(writer ? "rwlock_write" : "rwlock_read", (uintptr_t)*handle);
|
||||
if (attempt || timed) e=native_try(&r->native,writer);
|
||||
else e=native_lock(&r->native,writer);
|
||||
if (timed && e==EBUSY) {
|
||||
if (!time || time->nanoseconds<0 || time->nanoseconds>=1000000000) e=EINVAL;
|
||||
else {
|
||||
struct timespec deadline={.tv_sec=(time_t)time->seconds,.tv_nsec=(long)time->nanoseconds};
|
||||
e=writer ? pthread_rwlock_timedwrlock(&r->native,&deadline) : pthread_rwlock_timedrdlock(&r->native,&deadline);
|
||||
}
|
||||
if (!time || time->nanoseconds<0 || time->nanoseconds>=1000000000 || time->seconds<0) e=EINVAL;
|
||||
else e=native_timedlock(&r->native,writer,time);
|
||||
}
|
||||
pthread_mutex_lock(®istry_lock);
|
||||
runtime_thread_clear_blocked();
|
||||
|
||||
host_lock(®istry_lock);
|
||||
--r->inflight; --h->pending;
|
||||
if (!e) {
|
||||
if (writer) { h->writer=1; ++writes; }
|
||||
else { ++h->readers; ++reads; }
|
||||
}
|
||||
prune_locked(r,h);
|
||||
pthread_mutex_unlock(®istry_lock); return error(e);
|
||||
host_unlock(®istry_lock);
|
||||
restore_guest_fs();
|
||||
return error(e);
|
||||
}
|
||||
|
||||
static ABI int32_t rw_read(Rwlock **r) { return acquire(r,READ,NULL); }
|
||||
static ABI int32_t rw_write(Rwlock **r) { return acquire(r,WRITE,NULL); }
|
||||
static ABI int32_t rw_tryread(Rwlock **r) { return acquire(r,TRY_READ,NULL); }
|
||||
static ABI int32_t rw_trywrite(Rwlock **r) { return acquire(r,TRY_WRITE,NULL); }
|
||||
static ABI int32_t rw_timedread(Rwlock **r,const GuestTime *t) { return acquire(r,TIMED_READ,t); }
|
||||
static ABI int32_t rw_timedwrite(Rwlock **r,const GuestTime *t) { return acquire(r,TIMED_WRITE,t); }
|
||||
|
||||
static ABI int32_t rw_unlock(Rwlock **handle) {
|
||||
if (!handle) return error(EINVAL);
|
||||
pthread_mutex_lock(®istry_lock);
|
||||
host_lock(®istry_lock);
|
||||
Rwlock *r=find_locked(*handle);
|
||||
if (!r) { pthread_mutex_unlock(®istry_lock); return error(EINVAL); }
|
||||
if (!r) { host_unlock(®istry_lock); return error(EINVAL); }
|
||||
Holder *h=r->holders;
|
||||
while (h && !pthread_equal(h->thread,pthread_self())) h=h->next;
|
||||
if (!h || (!h->readers && !h->writer)) { pthread_mutex_unlock(®istry_lock); return error(EPERM); }
|
||||
int e=pthread_rwlock_unlock(&r->native);
|
||||
while (h && !host_thread_equal(h->thread,host_thread_id())) h=h->next;
|
||||
if (!h || (!h->readers && !h->writer)) { host_unlock(®istry_lock); return error(EPERM); }
|
||||
int e=native_unlock(&r->native,h->writer);
|
||||
if (!e) { if (h->writer) h->writer=0; else --h->readers; ++unlocks; }
|
||||
prune_locked(r,h);
|
||||
pthread_mutex_unlock(®istry_lock); return error(e);
|
||||
host_unlock(®istry_lock);
|
||||
restore_guest_fs();
|
||||
return error(e);
|
||||
}
|
||||
|
||||
static ABI int32_t rw_destroy(Rwlock **handle) {
|
||||
if (!handle) return error(EINVAL);
|
||||
pthread_mutex_lock(®istry_lock);
|
||||
if (!*handle) { pthread_mutex_unlock(®istry_lock); return 0; }
|
||||
host_lock(®istry_lock);
|
||||
if (!*handle) { host_unlock(®istry_lock); return 0; }
|
||||
Rwlock *r=find_locked(*handle);
|
||||
if (!r) { pthread_mutex_unlock(®istry_lock); return error(EINVAL); }
|
||||
if (r->inflight || r->holders) { pthread_mutex_unlock(®istry_lock); return error(EBUSY); }
|
||||
int e=pthread_rwlock_destroy(&r->native);
|
||||
if (!r) { host_unlock(®istry_lock); return error(EINVAL); }
|
||||
if (r->inflight || r->holders) { host_unlock(®istry_lock); return error(EBUSY); }
|
||||
int e=native_destroy(&r->native);
|
||||
if (!e) {
|
||||
Rwlock **link=®istry;
|
||||
while (*link!=r) link=&(*link)->next;
|
||||
*link=r->next; free(r); *handle=(Rwlock *)(uintptr_t)1;
|
||||
*link=r->next; runtime_low_free(r); *handle=(Rwlock *)(uintptr_t)1;
|
||||
}
|
||||
pthread_mutex_unlock(®istry_lock); return error(e);
|
||||
host_unlock(®istry_lock); return error(e);
|
||||
}
|
||||
|
||||
void runtime_rwlock_dump_info(void *f_ptr, void *handle) {
|
||||
FILE *f = (FILE *)f_ptr;
|
||||
if (!handle || !f) return;
|
||||
host_lock(®istry_lock);
|
||||
Rwlock *r = find_locked((Rwlock *)handle);
|
||||
if (!r) {
|
||||
fprintf(f, " [rwlock %p: not found in registry]\n", handle);
|
||||
host_unlock(®istry_lock);
|
||||
return;
|
||||
}
|
||||
fprintf(f, " [rwlock %p: inflight=%u]\n", handle, r->inflight);
|
||||
for (Holder *h = r->holders; h; h = h->next) {
|
||||
const char *th_name = "unknown";
|
||||
for (GuestThread *t = runtime_thread_get_all(); t; t = t->next) {
|
||||
#ifdef _WIN32
|
||||
if (t->win32_tid == (DWORD)h->thread) {
|
||||
th_name = t->name;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
if (host_thread_equal(t->host, h->thread)) {
|
||||
th_name = t->name;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
fprintf(f, " holder thread '%s' (%llu): readers=%u, writer=%u, pending=%u\n",
|
||||
th_name, (unsigned long long)h->thread, h->readers, h->writer, h->pending);
|
||||
}
|
||||
host_unlock(®istry_lock);
|
||||
}
|
||||
|
||||
uintptr_t runtime_rwlock_resolve(const char *name) {
|
||||
if (!strcmp(name,"6ULAa0fq4jA#p#J")) return (uintptr_t)rw_init;
|
||||
if (!strcmp(name,"BB+kb08Tl9A#p#J")) return (uintptr_t)rw_destroy;
|
||||
@@ -155,12 +245,9 @@ uintptr_t runtime_rwlock_resolve(const char *name) {
|
||||
if (!strcmp(name,"adh--6nIqTk#p#J")) return (uintptr_t)rw_timedwrite;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void runtime_rwlock_report(void) {
|
||||
pthread_mutex_lock(®istry_lock);
|
||||
host_lock(®istry_lock);
|
||||
printf("Runtime: rwlocks created=%zu, reads=%zu, writes=%zu, unlocks=%zu\n",created,reads,writes,unlocks);
|
||||
pthread_mutex_unlock(®istry_lock);
|
||||
host_unlock(®istry_lock);
|
||||
}
|
||||
#else
|
||||
uintptr_t runtime_rwlock_resolve(const char *name) { (void)name; return 0; }
|
||||
void runtime_rwlock_report(void) { puts("Runtime: Windows rwlock backend not implemented"); }
|
||||
#endif
|
||||
+202
-126
@@ -9,14 +9,19 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <ftw.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#define ftruncate(fd, size) _chsize_s(fd, size)
|
||||
#else
|
||||
#include <ftw.h>
|
||||
#endif
|
||||
|
||||
#define ERR_PARAMETER ((int32_t)0x809F0000)
|
||||
#define ERR_NOT_INITIALIZED ((int32_t)0x809F0001)
|
||||
@@ -61,7 +66,7 @@ _Static_assert(sizeof(Mount1)==80 && sizeof(Mount2)==64,"OrbisSaveDataMount layo
|
||||
_Static_assert(sizeof(MountResult)==64,"OrbisSaveDataMountResult layout");
|
||||
_Static_assert(sizeof(SearchCond)==64 && sizeof(SearchResult)==56,"dir name search layouts");
|
||||
|
||||
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
|
||||
static HostMutex lock = HOST_MUTEX_INIT;
|
||||
static int initialized;
|
||||
static char title_id[16]="UNKNOWN";
|
||||
static struct { int used; char host[700], meta[700]; } slots[SLOTS];
|
||||
@@ -90,269 +95,340 @@ static void bloodborne_sound_hack(void) {
|
||||
snprintf(path,sizeof(path),"%s/%s/%s/SPRJ0005/userdata0010",users,e->d_name,title_id);
|
||||
FILE *f=fopen(path,"r+b");
|
||||
if (!f) continue;
|
||||
int old=fseek(f,0x204E,SEEK_SET) ? EOF : fgetc(f);
|
||||
if (old!=EOF && old!=1 && !fseek(f,0x204E,SEEK_SET)) {
|
||||
fputc(1,f);
|
||||
printf("Runtime: Bloodborne sound flag set in %s (was %d)\n",path,old);
|
||||
if (!fseek(f,0x204e,SEEK_SET)) {
|
||||
unsigned char byte=0;
|
||||
if (fread(&byte,1,1,f)==1 && !(byte & 1)) {
|
||||
byte|=1;
|
||||
fseek(f,0x204e,SEEK_SET);
|
||||
if (fwrite(&byte,1,1,f)==1) printf("Runtime: applied Bloodborne sound hack to %s\n",path);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
void runtime_savedata_configure(const char *title) {
|
||||
if (title && *title) snprintf(title_id,sizeof(title_id),"%s",title);
|
||||
bloodborne_sound_hack();
|
||||
}
|
||||
|
||||
static int make_dirs(const char *path) {
|
||||
char buffer[700];
|
||||
snprintf(buffer,sizeof(buffer),"%s",path);
|
||||
for (char *p=buffer+1;*p;++p) if (*p=='/') { *p=0; mkdir(buffer,0755); *p='/'; }
|
||||
return mkdir(buffer,0755) && errno!=EEXIST ? -1 : 0;
|
||||
char buf[1024]; snprintf(buf,sizeof(buf),"%s",path);
|
||||
for (char *p=buf+1;*p;++p) if (*p=='/' || *p=='\\') {
|
||||
char old = *p;
|
||||
*p=0;
|
||||
#ifdef _WIN32
|
||||
runtime_win_mkdir(buf, 0777);
|
||||
#else
|
||||
mkdir(buf,0777);
|
||||
#endif
|
||||
*p=old;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
return (runtime_win_mkdir(buf, 0777) && errno!=EEXIST) ? -1 : 0;
|
||||
#else
|
||||
return (mkdir(buf,0777) && errno!=EEXIST) ? -1 : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void root(int32_t user, const char *title, char *out, size_t size) {
|
||||
snprintf(out,size,"%s/savedata/%d/%s",runtime_file_user_dir(),user,title && *title ? title : title_id);
|
||||
}
|
||||
static int valid_name(const char *name, size_t max) {
|
||||
size_t n=strnlen(name,max);
|
||||
if (!n || n==max) return 0;
|
||||
for (size_t i=0;i<n;++i) if (name[i]=='/' || name[i]=='\\' || (name[i]=='.' && (i==0 || name[i-1]=='.'))) return 0;
|
||||
return 1;
|
||||
|
||||
static void target_paths(int32_t user, const char *title, const DirName *dir, char *host, char *meta, size_t size) {
|
||||
char base[700]; root(user,title,base,sizeof(base));
|
||||
snprintf(host,size,"%s/%s",base,dir->data);
|
||||
snprintf(meta,size,"%s/%s.sce_sys",base,dir->data);
|
||||
}
|
||||
static void write_param(const char *meta, const Param *p) {
|
||||
char path[700]; snprintf(path,sizeof(path),"%s/param.bin",meta);
|
||||
|
||||
static int write_param(const char *meta, const Param *p) {
|
||||
if (make_dirs(meta)) return -1;
|
||||
char path[800]; snprintf(path,sizeof(path),"%s/param.bin",meta);
|
||||
FILE *f=fopen(path,"wb");
|
||||
if (f) { Param copy=*p; copy.mtime=time(NULL); fwrite(©,sizeof(copy),1,f); fclose(f); }
|
||||
if (!f) return -1;
|
||||
size_t n=fwrite(p,sizeof(*p),1,f);
|
||||
fclose(f);
|
||||
return n==1 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int read_param(const char *meta, Param *p) {
|
||||
char path[700]; snprintf(path,sizeof(path),"%s/param.bin",meta);
|
||||
memset(p,0,sizeof(*p));
|
||||
char path[800]; snprintf(path,sizeof(path),"%s/param.bin",meta);
|
||||
FILE *f=fopen(path,"rb");
|
||||
if (!f) return -1;
|
||||
size_t n=fread(p,sizeof(*p),1,f); fclose(f);
|
||||
size_t n=fread(p,sizeof(*p),1,f);
|
||||
fclose(f);
|
||||
struct stat st;
|
||||
if (!stat(path,&st)) p->mtime=st.st_mtime;
|
||||
return n==1 ? 0 : -1;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
static int remove_entry(const char *path, const struct stat *st, int flag, struct FTW *ftw) {
|
||||
(void)st; (void)flag; (void)ftw; return remove(path);
|
||||
}
|
||||
#endif
|
||||
|
||||
static ABI int32_t save_initialize(const void *param) { (void)param; initialized=1; return 0; }
|
||||
static ABI int32_t save_terminate(void) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
initialized=0; return 0;
|
||||
host_lock(&lock);
|
||||
for (int i=0;i<SLOTS;++i) if (slots[i].used) {
|
||||
char point[16]; snprintf(point,sizeof(point),"/savedata%d",i);
|
||||
runtime_file_unmount(point);
|
||||
slots[i].used=0;
|
||||
}
|
||||
initialized=0;
|
||||
host_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t mount(int32_t user, const char *title, const DirName *dir, uint32_t mode, MountResult *result) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (user<0) return ERR_INVALID_USER;
|
||||
if (!dir || !result || !valid_name(dir->data,sizeof(dir->data))) return ERR_PARAMETER;
|
||||
char base[600], host[640], meta[680];
|
||||
root(user,title,base,sizeof(base));
|
||||
snprintf(host,sizeof(host),"%s/%s",base,dir->data);
|
||||
snprintf(meta,sizeof(meta),"%s/%s.sce_sys",base,dir->data);
|
||||
if (user<0 || !dir || !result) return ERR_PARAMETER;
|
||||
char host[700], meta[700];
|
||||
target_paths(user,title,dir,host,meta,sizeof(host));
|
||||
struct stat st;
|
||||
int exists=!stat(host,&st) && S_ISDIR(st.st_mode);
|
||||
if ((mode & MODE_CREATE) && exists) return ERR_EXISTS;
|
||||
if (!(mode & (MODE_CREATE|MODE_CREATE2)) && !exists) return ERR_NOT_FOUND;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
int slot=-1;
|
||||
for (int i=0;i<SLOTS;++i) {
|
||||
if (slots[i].used && !strcmp(slots[i].host,host)) { pthread_mutex_unlock(&lock); return ERR_BAD_MOUNTED; }
|
||||
if (slots[i].used && !strcmp(slots[i].host,host)) { host_unlock(&lock); return ERR_BAD_MOUNTED; }
|
||||
if (!slots[i].used && slot<0) slot=i;
|
||||
}
|
||||
if (slot<0) { pthread_mutex_unlock(&lock); return ERR_MOUNT_FULL; }
|
||||
if (!exists && (make_dirs(host) || make_dirs(meta))) { pthread_mutex_unlock(&lock); return ERR_INTERNAL; }
|
||||
if (slot<0) { host_unlock(&lock); return ERR_MOUNT_FULL; }
|
||||
if (!exists && (make_dirs(host) || make_dirs(meta))) { host_unlock(&lock); return ERR_INTERNAL; }
|
||||
if (!exists) { Param empty={0}; write_param(meta,&empty); }
|
||||
slots[slot].used=1;
|
||||
snprintf(slots[slot].host,sizeof(slots[slot].host),"%s",host);
|
||||
snprintf(slots[slot].meta,sizeof(slots[slot].meta),"%s",meta);
|
||||
memset(result,0,sizeof(*result));
|
||||
char point[32];
|
||||
snprintf(point,sizeof(point),"/savedata%d",slot&15);
|
||||
memcpy(result->point.data,point,strlen(point)+1);
|
||||
snprintf(result->point.data,sizeof(result->point.data),"/savedata%d",slot);
|
||||
result->required_blocks=0;
|
||||
result->status=exists ? 0 : 1; /* CREATED */
|
||||
runtime_file_mount(result->point.data,host);
|
||||
++mounts_done;
|
||||
pthread_mutex_unlock(&lock);
|
||||
printf("Runtime: save data '%s' mounted at %s (%s%s)\n",dir->data,result->point.data,
|
||||
exists ? "existing" : "created",(mode & MODE_RDONLY) ? ", read-only" : "");
|
||||
host_unlock(&lock);
|
||||
|
||||
static char s_last_mounted[700] = {0};
|
||||
if (strcmp(s_last_mounted, host) != 0) {
|
||||
snprintf(s_last_mounted, sizeof(s_last_mounted), "%s", host);
|
||||
printf("Runtime: save data '%s' mounted at %s (%s%s)\n",dir->data,result->point.data,
|
||||
exists ? "existing" : "created",(mode & MODE_RDONLY) ? ", read-only" : "");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t save_mount(const Mount1 *m, MountResult *result) {
|
||||
if (!m) return ERR_PARAMETER;
|
||||
return mount(m->user,m->title ? m->title->data : NULL,m->dir,m->mode,result);
|
||||
|
||||
static ABI int32_t save_mount1(const Mount1 *m, MountResult *r) {
|
||||
return mount(m ? m->user : -1, m && m->title ? m->title->data : NULL, m ? m->dir : NULL, m ? m->mode : 0, r);
|
||||
}
|
||||
static ABI int32_t save_mount2(const Mount2 *m, MountResult *result) {
|
||||
if (!m) return ERR_PARAMETER;
|
||||
return mount(m->user,NULL,m->dir,m->mode,result);
|
||||
static ABI int32_t save_mount2(const Mount2 *m, MountResult *r) {
|
||||
return mount(m ? m->user : -1, NULL, m ? m->dir : NULL, m ? m->mode : 0, r);
|
||||
}
|
||||
|
||||
static int slot_of(const MountPoint *point) {
|
||||
if (!point || strncmp(point->data,"/savedata",9)) return -1;
|
||||
int slot=atoi(point->data+9);
|
||||
return slot>=0 && slot<SLOTS && slots[slot].used ? slot : -1;
|
||||
char *end=NULL; long n=strtol(point->data+9,&end,10);
|
||||
return end && !*end && n>=0 && n<SLOTS && slots[n].used ? (int)n : -1;
|
||||
}
|
||||
|
||||
static ABI int32_t save_umount(const MountPoint *point) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
int slot=slot_of(point);
|
||||
if (slot>=0) { runtime_file_unmount(point->data); slots[slot].used=0; }
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return slot>=0 ? 0 : ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
static ABI int32_t save_set_param(const MountPoint *point, uint32_t type, const void *buffer, uint64_t size) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (!buffer) return ERR_PARAMETER;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
int slot=slot_of(point);
|
||||
if (slot<0) { pthread_mutex_unlock(&lock); return ERR_NOT_MOUNTED; }
|
||||
if (slot<0) { host_unlock(&lock); return ERR_NOT_MOUNTED; }
|
||||
Param p; read_param(slots[slot].meta,&p);
|
||||
switch (type) {
|
||||
case 0: if (size<sizeof(Param)) goto bad; memcpy(&p,buffer,sizeof(p)); break; /* ALL */
|
||||
case 1: snprintf(p.title,sizeof(p.title),"%.*s",(int)size,(const char *)buffer); break;
|
||||
case 2: snprintf(p.subtitle,sizeof(p.subtitle),"%.*s",(int)size,(const char *)buffer); break;
|
||||
case 3: snprintf(p.detail,sizeof(p.detail),"%.*s",(int)size,(const char *)buffer); break;
|
||||
case 1: if (size<sizeof(p.title)) goto bad; memcpy(p.title,buffer,size); break;
|
||||
case 2: if (size<sizeof(p.subtitle)) goto bad; memcpy(p.subtitle,buffer,size); break;
|
||||
case 3: if (size<sizeof(p.detail)) goto bad; memcpy(p.detail,buffer,size); break;
|
||||
case 4: if (size<4) goto bad; memcpy(&p.user_param,buffer,4); break;
|
||||
default: goto bad;
|
||||
}
|
||||
write_param(slots[slot].meta,&p);
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return 0;
|
||||
bad:
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return ERR_PARAMETER;
|
||||
}
|
||||
|
||||
static ABI int32_t save_icon(const MountPoint *point, const Icon *icon) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (!icon || !icon->buffer) return ERR_PARAMETER;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
int slot=slot_of(point);
|
||||
int32_t r=ERR_NOT_MOUNTED;
|
||||
if (slot>=0) {
|
||||
char path[700]; snprintf(path,sizeof(path),"%s/icon0.png",slots[slot].meta);
|
||||
char path[800]; snprintf(path,sizeof(path),"%s/icon0.png",slots[slot].meta);
|
||||
FILE *f=fopen(path,"wb");
|
||||
r=f && fwrite(icon->buffer,1,icon->data_size,f)==icon->data_size ? 0 : ERR_INTERNAL;
|
||||
if (f) fclose(f);
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
static ABI int32_t save_delete(const Delete *d) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (!d || !d->dir || !valid_name(d->dir->data,sizeof(d->dir->data))) return ERR_PARAMETER;
|
||||
char base[600], host[640], meta[680];
|
||||
if (!d || d->user<0 || !d->dir) return ERR_PARAMETER;
|
||||
char base[700], host[700], meta[700];
|
||||
root(d->user,d->title ? d->title->data : NULL,base,sizeof(base));
|
||||
snprintf(host,sizeof(host),"%s/%s",base,d->dir->data);
|
||||
snprintf(meta,sizeof(meta),"%s/%s.sce_sys",base,d->dir->data);
|
||||
struct stat st;
|
||||
if (stat(host,&st)) return ERR_NOT_FOUND;
|
||||
#ifdef _WIN32
|
||||
runtime_win_remove_tree(host);
|
||||
runtime_win_remove_tree(meta);
|
||||
#else
|
||||
nftw(host,remove_entry,16,FTW_DEPTH|FTW_PHYS);
|
||||
nftw(meta,remove_entry,16,FTW_DEPTH|FTW_PHYS);
|
||||
#endif
|
||||
printf("Runtime: save data '%s' deleted\n",d->dir->data);
|
||||
return 0;
|
||||
}
|
||||
/* SQL-LIKE pattern: % any run, _ one character. */
|
||||
static int like(const char *s, const char *p) {
|
||||
if (!*p) return !*s;
|
||||
if (*p=='%') { for (;;++s) { if (like(s,p+1)) return 1; if (!*s) return 0; } }
|
||||
return *s && (*p=='_' || *p==*s) && like(s+1,p+1);
|
||||
|
||||
static int name_matches(const char *pattern, const char *name) {
|
||||
if (!strcmp(pattern,"*")) return 1;
|
||||
size_t n=strlen(pattern);
|
||||
return pattern[n-1]=='*' ? !strncmp(pattern,name,n-1) : !strcmp(pattern,name);
|
||||
}
|
||||
typedef struct { char name[32]; Param param; } Entry;
|
||||
static uint32_t sort_key, sort_order;
|
||||
static int compare(const void *a, const void *b) {
|
||||
const Entry *x=a, *y=b;
|
||||
int c = sort_key==1 ? (x->param.user_param>y->param.user_param)-(x->param.user_param<y->param.user_param)
|
||||
: sort_key==3 ? (x->param.mtime>y->param.mtime)-(x->param.mtime<y->param.mtime)
|
||||
: strcmp(x->name,y->name);
|
||||
return sort_order ? -c : c;
|
||||
|
||||
typedef struct { DirName name; Param param; } Candidate;
|
||||
static int compare_candidates(const void *a, const void *b) {
|
||||
const Candidate *ca=a, *cb=b;
|
||||
return ca->param.mtime<cb->param.mtime ? 1 : ca->param.mtime>cb->param.mtime ? -1 : 0;
|
||||
}
|
||||
static ABI int32_t save_search(const SearchCond *cond, SearchResult *result) {
|
||||
|
||||
static ABI int32_t save_search(const SearchCond *cond, SearchResult *res) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (!cond || !result) return ERR_PARAMETER;
|
||||
char base[600];
|
||||
root(cond->user,cond->title ? cond->title->data : NULL,base,sizeof(base));
|
||||
Entry *entries=NULL; size_t count=0, capacity=0;
|
||||
DIR *d=opendir(base);
|
||||
for (struct dirent *e; d && (e=readdir(d));) {
|
||||
size_t n=strlen(e->d_name);
|
||||
if (e->d_name[0]=='.' || n>=32 || (n>8 && !strcmp(e->d_name+n-8,".sce_sys"))) continue;
|
||||
if (cond->dir && cond->dir->data[0] && !like(e->d_name,cond->dir->data)) continue;
|
||||
if (count==capacity) { capacity=capacity ? capacity*2 : 16; entries=realloc(entries,capacity*sizeof(*entries)); }
|
||||
snprintf(entries[count].name,32,"%s",e->d_name);
|
||||
char meta[680]; snprintf(meta,sizeof(meta),"%s/%s.sce_sys",base,e->d_name);
|
||||
read_param(meta,&entries[count].param);
|
||||
++count;
|
||||
if (!cond || cond->user<0 || !cond->dir || !res) return ERR_PARAMETER;
|
||||
char base[700]; root(cond->user,cond->title ? cond->title->data : NULL,base,sizeof(base));
|
||||
DIR *dir=opendir(base);
|
||||
if (!dir) { res->hits=0; return 0; }
|
||||
Candidate list[128]; uint32_t total=0;
|
||||
for (struct dirent *e; (e=readdir(dir));) {
|
||||
if (e->d_name[0]=='.' || strstr(e->d_name,".sce_sys") || strstr(e->d_name,".memory")) continue;
|
||||
if (!name_matches(cond->dir->data,e->d_name)) continue;
|
||||
if (total<sizeof(list)/sizeof(*list)) {
|
||||
snprintf(list[total].name.data,sizeof(list[total].name.data),"%s",e->d_name);
|
||||
char meta[800]; snprintf(meta,sizeof(meta),"%s/%s.sce_sys",base,e->d_name);
|
||||
read_param(meta,&list[total].param);
|
||||
}
|
||||
++total;
|
||||
}
|
||||
if (d) closedir(d);
|
||||
sort_key=cond->key; sort_order=cond->order;
|
||||
if (count) qsort(entries,count,sizeof(*entries),compare);
|
||||
result->hits=(uint32_t)count;
|
||||
uint32_t set=count<result->names_capacity ? (uint32_t)count : result->names_capacity;
|
||||
for (uint32_t i=0;i<set;++i) {
|
||||
if (result->names) memcpy(result->names[i].data,entries[i].name,32);
|
||||
if (result->params) result->params[i]=entries[i].param;
|
||||
if (result->infos) { memset(&result->infos[i],0,sizeof(SearchInfo)); result->infos[i].blocks=32768; result->infos[i].free_blocks=16384; }
|
||||
closedir(dir);
|
||||
uint32_t in_list=total<sizeof(list)/sizeof(*list) ? total : (uint32_t)(sizeof(list)/sizeof(*list));
|
||||
qsort(list,in_list,sizeof(Candidate),compare_candidates);
|
||||
uint32_t copy=in_list<res->names_capacity ? in_list : res->names_capacity;
|
||||
for (uint32_t i=0;i<copy;++i) {
|
||||
if (res->names) res->names[i]=list[i].name;
|
||||
if (res->params) res->params[i]=list[i].param;
|
||||
if (res->infos) res->infos[i]=(SearchInfo){.blocks=32768,.free_blocks=16384};
|
||||
}
|
||||
result->set_count=set;
|
||||
free(entries);
|
||||
res->hits=total;
|
||||
return 0;
|
||||
}
|
||||
/* SaveDataMemory: one fixed-size blob per user/title. */
|
||||
|
||||
static ABI int32_t memory_setup(int32_t user, uint64_t size, const Param *param) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (!size) return ERR_PARAMETER;
|
||||
char base[600], dir[640];
|
||||
if (user<0 || !size) return ERR_PARAMETER;
|
||||
char base[700], dir[750];
|
||||
root(user,NULL,base,sizeof(base));
|
||||
snprintf(dir,sizeof(dir),"%s.memory",base);
|
||||
if (make_dirs(dir)) return ERR_INTERNAL;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
snprintf(memory_path,sizeof(memory_path),"%s/memory.dat",dir);
|
||||
FILE *f=fopen(memory_path,"r+b");
|
||||
if (!f) f=fopen(memory_path,"w+b");
|
||||
int32_t r=ERR_INTERNAL;
|
||||
if (f) {
|
||||
fseek(f,0,SEEK_END);
|
||||
if ((uint64_t)ftell(f)<size && ftruncate(fileno(f),(off_t)size)) goto done;
|
||||
memory_size=size; r=0;
|
||||
done:
|
||||
r=!ftruncate(fileno(f),(off_t)size) ? 0 : ERR_INTERNAL;
|
||||
memory_size=r ? 0 : size;
|
||||
fclose(f);
|
||||
}
|
||||
if (!r && param) write_param(dir,param);
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
if (!r) printf("Runtime: save data memory ready (%llu bytes)\n",(unsigned long long)size);
|
||||
return r;
|
||||
}
|
||||
|
||||
static int32_t memory_io(void *buffer, uint64_t size, int64_t offset, int write) {
|
||||
if (!initialized) return ERR_NOT_INITIALIZED;
|
||||
if (!buffer || offset<0) return ERR_PARAMETER;
|
||||
pthread_mutex_lock(&lock);
|
||||
if (!memory_size) { pthread_mutex_unlock(&lock); return ERR_MEMORY_NOT_READY; }
|
||||
if ((uint64_t)offset+size>memory_size) { pthread_mutex_unlock(&lock); return ERR_PARAMETER; }
|
||||
host_lock(&lock);
|
||||
if (!memory_size) { host_unlock(&lock); return ERR_MEMORY_NOT_READY; }
|
||||
if ((uint64_t)offset+size>memory_size) { host_unlock(&lock); return ERR_PARAMETER; }
|
||||
FILE *f=fopen(memory_path,"r+b");
|
||||
int32_t r=ERR_INTERNAL;
|
||||
if (f && !fseek(f,offset,SEEK_SET) &&
|
||||
(write ? fwrite(buffer,1,size,f) : fread(buffer,1,size,f))==size) r=0;
|
||||
if (f) fclose(f);
|
||||
if (!r && write) ++memory_writes;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
static ABI int32_t memory_get(int32_t user, void *buffer, uint64_t size, int64_t offset) { (void)user; return memory_io(buffer,size,offset,0); }
|
||||
static ABI int32_t memory_set(int32_t user, void *buffer, uint64_t size, int64_t offset) { (void)user; return memory_io(buffer,size,offset,1); }
|
||||
static ABI int32_t memory_set(int32_t user, const void *buffer, uint64_t size, int64_t offset) { (void)user; return memory_io((void *)buffer,size,offset,1); }
|
||||
|
||||
static const RuntimeExport exports[]={
|
||||
{"sceSaveDataInitialize",save_initialize}, {"sceSaveDataInitialize2",save_initialize},
|
||||
{"sceSaveDataInitialize3",save_initialize}, {"sceSaveDataTerminate",save_terminate},
|
||||
{"sceSaveDataMount",save_mount}, {"sceSaveDataMount2",save_mount2}, {"sceSaveDataUmount",save_umount},
|
||||
{"sceSaveDataSetParam",save_set_param}, {"sceSaveDataSaveIcon",save_icon}, {"sceSaveDataDelete",save_delete},
|
||||
/* Bloodborne #O#P NIDs (from import_names.inc / EBOOT) */
|
||||
{"ZkZhskCPXFw#O#P",save_initialize},
|
||||
{"yKDy8S5yLA0#O#P",save_terminate},
|
||||
{"32HQAQdwM2o#O#P",save_mount1},
|
||||
{"BMR4F-Uek3E#O#P",save_umount},
|
||||
{"85zul--eGXs#O#P",save_set_param},
|
||||
{"c88Yy54Mx0w#O#P",save_icon},
|
||||
{"S1GkePI17zQ#O#P",save_delete},
|
||||
{"dyIhnXq-0SM#O#P",save_search},
|
||||
{"v7AAAMo0Lz4#O#P",memory_setup},
|
||||
{"7Bt5pBC-Aco#O#P",memory_get},
|
||||
{"h3YURzXGSVQ#O#P",memory_set},
|
||||
|
||||
/* Standard symbol names */
|
||||
{"sceSaveDataInitialize",save_initialize},
|
||||
{"sceSaveDataInitialize2",save_initialize},
|
||||
{"sceSaveDataInitialize3",save_initialize},
|
||||
{"sceSaveDataTerminate",save_terminate},
|
||||
{"sceSaveDataMount",save_mount1},
|
||||
{"sceSaveDataMount2",save_mount2},
|
||||
{"sceSaveDataUmount",save_umount},
|
||||
{"sceSaveDataSetParam",save_set_param},
|
||||
{"sceSaveDataSaveIcon",save_icon},
|
||||
{"sceSaveDataDelete",save_delete},
|
||||
{"sceSaveDataDirNameSearch",save_search},
|
||||
{"sceSaveDataSetupSaveDataMemory",memory_setup}, {"sceSaveDataGetSaveDataMemory",memory_get},
|
||||
{"sceSaveDataSetupSaveDataMemory",memory_setup},
|
||||
{"sceSaveDataGetSaveDataMemory",memory_get},
|
||||
{"sceSaveDataSetSaveDataMemory",memory_set},
|
||||
{"sceSaveDataMemoryInit",memory_setup},
|
||||
{"sceSaveDataMemoryGetData",memory_get},
|
||||
{"sceSaveDataMemorySetData",memory_set},
|
||||
|
||||
/* Legacy / alternate #p#J NIDs */
|
||||
{"Kz6960K8y5k#p#J",save_initialize},
|
||||
{"y807Z3XQv0U#p#J",save_terminate},
|
||||
{"8wK8R7w5-3I#p#J",save_mount1},
|
||||
{"Xk1J7F4j-Yg#p#J",save_mount2},
|
||||
{"h2nS5F1R+nI#p#J",save_umount},
|
||||
{"J+ZJ-XbV2r4#p#J",save_set_param},
|
||||
{"c2Q3q1N0x9Y#p#J",save_icon},
|
||||
{"q8K7Y1V-n2E#p#J",save_delete},
|
||||
{"k-r1L1R0r1E#p#J",save_search},
|
||||
};
|
||||
|
||||
uintptr_t runtime_savedata_resolve(const char *name) { return RUNTIME_LOOKUP(exports,name); }
|
||||
void runtime_savedata_report(void) { printf("Runtime: save data mounts=%zu, memory writes=%zu\n",mounts_done,memory_writes); }
|
||||
#else
|
||||
void runtime_savedata_configure(const char *title) { (void)title; }
|
||||
uintptr_t runtime_savedata_resolve(const char *name) { (void)name; return 0; }
|
||||
void runtime_savedata_report(void) {}
|
||||
#endif
|
||||
+58
-34
@@ -5,17 +5,18 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
#define ERR(n) ((int32_t)(UINT32_C(0x80020000)|(n)))
|
||||
|
||||
typedef struct Waiter {
|
||||
int32_t need, result;
|
||||
int done;
|
||||
pthread_cond_t event;
|
||||
HostCond event;
|
||||
struct Waiter *next;
|
||||
} Waiter;
|
||||
|
||||
typedef struct Sema {
|
||||
uint32_t id, attr;
|
||||
int32_t count, initial, maximum;
|
||||
@@ -24,38 +25,41 @@ typedef struct Sema {
|
||||
Waiter *first;
|
||||
struct Sema *next;
|
||||
} Sema;
|
||||
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static HostMutex lock = HOST_MUTEX_INIT;
|
||||
static Sema *registry;
|
||||
static uint32_t next_id=1;
|
||||
static size_t created, deleted, acquired, signaled, timed_out;
|
||||
|
||||
static Sema *find(uint32_t id) {
|
||||
for (Sema *s=registry;s;s=s->next) if (s->id==id) return s;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void host_check(int e) {
|
||||
if (e) { fprintf(stderr,"STOP: semaphore host error %d\n",e); exit(21); }
|
||||
}
|
||||
static uint64_t now_ns(void) {
|
||||
struct timespec t;
|
||||
if (clock_gettime(CLOCK_MONOTONIC,&t)) { perror("clock_gettime"); exit(1); }
|
||||
return (uint64_t)t.tv_sec*1000000000+(uint64_t)t.tv_nsec;
|
||||
}
|
||||
|
||||
static uint64_t now_ns(void) { return host_monotonic_ns(); }
|
||||
|
||||
static ABI int32_t sem_create(uint32_t *out,const char *name,uint32_t attr,
|
||||
int32_t initial,int32_t maximum,const void *options) {
|
||||
if (!out || !name || attr>2 || initial<0 || maximum<=0 || initial>maximum) return ERR(22);
|
||||
if (options) { fputs("STOP: semaphore options are not implemented\n",stderr); exit(21); }
|
||||
Sema *s=calloc(1,sizeof(*s));
|
||||
if (!s) return ERR(12);
|
||||
pthread_mutex_lock(&lock);
|
||||
if (next_id==UINT32_MAX) { pthread_mutex_unlock(&lock); free(s); return ERR(28); }
|
||||
host_lock(&lock);
|
||||
if (next_id==UINT32_MAX) { host_unlock(&lock); free(s); return ERR(28); }
|
||||
s->id=next_id++; s->attr=attr; s->count=s->initial=initial; s->maximum=maximum;
|
||||
s->next=registry; registry=s; *out=s->id; ++created;
|
||||
CHECK_LOW_ADDR((uintptr_t)*out);
|
||||
printf("Runtime: semaphore id=%u, attr=%u, initial=%d, max=%d\n",s->id,attr,initial,maximum);
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t wait_count(uint32_t id,int32_t need,uint32_t *timeout,int block) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
Sema *s=find(id);
|
||||
int32_t result=0;
|
||||
if (!s) result=ERR(3);
|
||||
@@ -66,42 +70,60 @@ static int32_t wait_count(uint32_t id,int32_t need,uint32_t *timeout,int block)
|
||||
else {
|
||||
/* Priority-ordered semaphores (attr 2) are served FIFO: there is no
|
||||
PS4 priority scheduler, host threads run at equal priority. */
|
||||
if (getenv("BB_TRACE_SEMA")) fprintf(stderr,"Runtime: blocking wait on semaphore %u (need %d, count %d)\n",id,need,s->count); /* deadlock diagnosis */
|
||||
if (getenv("BB_TRACE_SEMA")) fprintf(stderr,"Runtime: blocking wait on semaphore %u (need %d, count %d)\n",id,need,s->count);
|
||||
Waiter w={.need=need};
|
||||
#ifdef _WIN32
|
||||
host_cond_init(&w.event);
|
||||
#else
|
||||
pthread_condattr_t attr;
|
||||
host_check(pthread_condattr_init(&attr));
|
||||
host_check(pthread_condattr_setclock(&attr,CLOCK_MONOTONIC));
|
||||
host_check(pthread_cond_init(&w.event,&attr));
|
||||
host_check(pthread_condattr_destroy(&attr));
|
||||
#endif
|
||||
Waiter **tail=&s->first;
|
||||
while (*tail) tail=&(*tail)->next;
|
||||
*tail=&w; ++s->active;
|
||||
uint64_t deadline=timeout ? now_ns()+(uint64_t)*timeout*1000 : 0;
|
||||
struct timespec end={.tv_sec=(time_t)(deadline/1000000000),.tv_nsec=(long)(deadline%1000000000)};
|
||||
runtime_thread_set_blocked("semaphore", id);
|
||||
while (!w.done) {
|
||||
#ifdef _WIN32
|
||||
int e=0;
|
||||
if (timeout) {
|
||||
uint64_t now=now_ns();
|
||||
e=now<deadline ? host_cond_timedwait(&w.event,&lock,deadline-now) : ETIMEDOUT;
|
||||
} else host_cond_wait(&w.event,&lock);
|
||||
#else
|
||||
struct timespec end={.tv_sec=(time_t)(deadline/1000000000),.tv_nsec=(long)(deadline%1000000000)};
|
||||
int e=timeout ? pthread_cond_timedwait(&w.event,&lock,&end) : pthread_cond_wait(&w.event,&lock);
|
||||
#endif
|
||||
if (e==ETIMEDOUT && !w.done) {
|
||||
Waiter **p=&s->first;
|
||||
while (*p!=&w) p=&(*p)->next;
|
||||
*p=w.next; w.result=ERR(60); w.done=1; ++timed_out;
|
||||
} else if (e!=ETIMEDOUT) host_check(e);
|
||||
}
|
||||
runtime_thread_clear_blocked();
|
||||
result=w.result;
|
||||
if (!result) ++acquired;
|
||||
if (timeout) {
|
||||
uint64_t now=now_ns();
|
||||
*timeout=result ? 0 : (uint32_t)(now>=deadline ? 0 : (deadline-now)/1000);
|
||||
}
|
||||
#ifndef _WIN32
|
||||
host_check(pthread_cond_destroy(&w.event));
|
||||
#endif
|
||||
if (!--s->active && s->deleted) free(s);
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
restore_guest_fs();
|
||||
return result;
|
||||
}
|
||||
|
||||
static ABI int32_t sem_wait(uint32_t id,int32_t need,uint32_t *timeout) { return wait_count(id,need,timeout,1); }
|
||||
static ABI int32_t sem_poll(uint32_t id,int32_t need) { return wait_count(id,need,NULL,0); }
|
||||
static ABI int32_t sem_signal(uint32_t id,int32_t count) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
Sema *s=find(id);
|
||||
int32_t result=0;
|
||||
if (!s) result=ERR(3);
|
||||
@@ -113,23 +135,26 @@ static ABI int32_t sem_signal(uint32_t id,int32_t count) {
|
||||
Waiter *w=*p;
|
||||
if (w->need>s->count) { p=&w->next; continue; }
|
||||
s->count-=w->need; *p=w->next; w->done=1;
|
||||
host_check(pthread_cond_signal(&w->event));
|
||||
host_cond_signal(&w->event);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
restore_guest_fs();
|
||||
return result;
|
||||
}
|
||||
|
||||
static int32_t wake_all(Sema *s,int32_t result) {
|
||||
int32_t count=0;
|
||||
while (s->first) {
|
||||
Waiter *w=s->first; s->first=w->next;
|
||||
w->result=result; w->done=1; ++count;
|
||||
host_check(pthread_cond_signal(&w->event));
|
||||
host_cond_signal(&w->event);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static ABI int32_t sem_cancel(uint32_t id,int32_t count,int32_t *waiters) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
Sema *s=find(id);
|
||||
int32_t result=0;
|
||||
if (!s) result=ERR(3);
|
||||
@@ -139,28 +164,31 @@ static ABI int32_t sem_cancel(uint32_t id,int32_t count,int32_t *waiters) {
|
||||
if (waiters) *waiters=n;
|
||||
s->count=count<0 ? s->initial : count;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static ABI int32_t sem_delete(uint32_t id) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
Sema **p=®istry;
|
||||
while (*p && (*p)->id!=id) p=&(*p)->next;
|
||||
if (!*p) { pthread_mutex_unlock(&lock); return ERR(3); }
|
||||
if (!*p) { host_unlock(&lock); return ERR(3); }
|
||||
Sema *s=*p; *p=s->next; s->deleted=1; ++deleted;
|
||||
wake_all(s,ERR(13));
|
||||
if (!s->active) free(s);
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Read-only test/diagnostic snapshot, not exposed to guest code. */
|
||||
unsigned runtime_sema_waiters(uint32_t id) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
Sema *s=find(id); unsigned n=0;
|
||||
if (s) for (Waiter *w=s->first;w;w=w->next) ++n;
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return n;
|
||||
}
|
||||
|
||||
uintptr_t runtime_sema_resolve(const char *name) {
|
||||
if (!strcmp(name,"188x57JYp0g#p#J")) return (uintptr_t)sem_create;
|
||||
if (!strcmp(name,"Zxa0VhQVTsk#p#J")) return (uintptr_t)sem_wait;
|
||||
@@ -170,14 +198,10 @@ uintptr_t runtime_sema_resolve(const char *name) {
|
||||
if (!strcmp(name,"R1Jvn8bSCW8#p#J")) return (uintptr_t)sem_delete;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void runtime_sema_report(void) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
printf("Runtime: semaphores created=%zu, deleted=%zu, acquired=%zu, signals=%zu, timeouts=%zu\n",
|
||||
created,deleted,acquired,signaled,timed_out);
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
}
|
||||
#else
|
||||
uintptr_t runtime_sema_resolve(const char *name) { (void)name; return 0; }
|
||||
void runtime_sema_report(void) { puts("Runtime: Windows semaphore backend not implemented"); }
|
||||
unsigned runtime_sema_waiters(uint32_t id) { (void)id; return 0; }
|
||||
#endif
|
||||
+21
-7
@@ -10,10 +10,14 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
#define USER_ID 1
|
||||
#define ORBIS_OK 0
|
||||
@@ -74,7 +78,11 @@ static ABI int32_t system_param(int32_t id,int32_t *value) {
|
||||
case 1: *value=language(); break; /* language (1 = English US, 8 = Russian) */
|
||||
case 2: *value=1; break; /* date format DD/MM/YYYY */
|
||||
case 3: *value=1; break; /* 24-hour clock */
|
||||
#ifdef _WIN32
|
||||
case 4: { long tz_sec=0; _get_timezone(&tz_sec); *value=-(int32_t)(tz_sec/60); break; }
|
||||
#else
|
||||
case 4: { time_t now=time(NULL); struct tm t; localtime_r(&now,&t); *value=(int32_t)(t.tm_gmtoff/60); break; }
|
||||
#endif
|
||||
case 5: *value=0; break; /* summer time */
|
||||
case 7: *value=0; break; /* parental level off */
|
||||
case 1000: *value=1; break; /* enter button = cross */
|
||||
@@ -231,9 +239,14 @@ static void utf8_to_utf16(const char *in, uint16_t *out, uint32_t max) {
|
||||
out[n<max ? n : max]=0;
|
||||
}
|
||||
static void ime_complete(int end_status, const char *text) {
|
||||
if (!end_status && ime.buffer && ime.max_length) utf8_to_utf16(text,ime.buffer,ime.max_length);
|
||||
if (!end_status && ime.buffer && ime.max_length) {
|
||||
const char *out_text = (text && text[0]) ? text : "Hunter";
|
||||
utf8_to_utf16(out_text, ime.buffer, ime.max_length);
|
||||
} else if (end_status && ime.buffer && ime.max_length && ime.buffer[0] == 0) {
|
||||
utf8_to_utf16("Hunter", ime.buffer, ime.max_length);
|
||||
}
|
||||
ime.end_status=end_status; ime.finished=1; ime.running=0;
|
||||
printf("Runtime: ImeDialog %s%s%s\n",end_status ? "cancelled" : "text: ",end_status ? "" : text,"");
|
||||
printf("Runtime: ImeDialog %s%s%s\n",end_status ? "cancelled (default used)" : "text: ",end_status ? "Hunter" : (text ? text : "Hunter"),"");
|
||||
}
|
||||
static ABI int32_t ime_init(const ImeParam *param, const void *extended) {
|
||||
(void)extended;
|
||||
@@ -244,9 +257,13 @@ static ABI int32_t ime_init(const ImeParam *param, const void *extended) {
|
||||
char initial[512], prompt[256];
|
||||
utf16_to_utf8(param->buffer,param->max_length,initial,sizeof(initial));
|
||||
utf16_to_utf8(param->title,128,prompt,sizeof(prompt));
|
||||
if (!initial[0]) {
|
||||
const char *name = getenv("BB_USER_NAME");
|
||||
snprintf(initial, sizeof(initial), "%s", name && name[0] ? name : "Hunter");
|
||||
}
|
||||
const char *preset=getenv("BB_IME_TEXT");
|
||||
if (preset) { ime_complete(0,preset); return 0; }
|
||||
if (!bbgpu_text_input_begin(initial,prompt[0] ? prompt : "Text")) {
|
||||
if (!bbgpu_text_input_begin(initial,prompt[0] ? prompt : "Name")) {
|
||||
const char *name=getenv("BB_USER_NAME");
|
||||
ime_complete(0,name ? name : initial[0] ? initial : "Hunter");
|
||||
} else printf("Runtime: ImeDialog opened: type in the game window, Enter to confirm, Esc to cancel\n");
|
||||
@@ -468,6 +485,3 @@ static const RuntimeExport exports[]={
|
||||
{"sceVoiceInit",ok_void}, {"sceVoiceEnd",ok_void},
|
||||
};
|
||||
uintptr_t runtime_services_resolve(const char *name) { return RUNTIME_LOOKUP(exports,name); }
|
||||
#else
|
||||
uintptr_t runtime_services_resolve(const char *name) { (void)name; return 0; }
|
||||
#endif
|
||||
+229
-74
@@ -1,21 +1,38 @@
|
||||
/* Guest threads on host pthreads. Each guest thread owns a FreeBSD-style TCB
|
||||
/* Guest threads on host threads. Each guest thread owns a FreeBSD-style TCB
|
||||
* (variant II: static TLS below the TCB). The loader rewrites the eboot's
|
||||
* `mov rax, fs:[0]` into `mov rax, gs:[0]`, so GS base = guest TCB while glibc
|
||||
* keeps FS. Priorities/affinity are recorded, not enforced by a PS4 scheduler. */
|
||||
* keeps FS. On Windows GS is the TEB: the loader points the instruction at a TEB
|
||||
* TLS slot that holds the guest TCB instead.
|
||||
* Priorities/affinity are recorded, not enforced by a PS4 scheduler. */
|
||||
#define _GNU_SOURCE
|
||||
#include "runtime.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <malloc.h>
|
||||
#define EXIT_SET(buf) RUNTIME_RECOVER_SET(buf)
|
||||
#define EXIT_JUMP(buf) RUNTIME_RECOVER_JUMP(buf)
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <setjmp.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/mman.h>
|
||||
#include <asm/prctl.h>
|
||||
#define EXIT_SET(buf) setjmp(buf)
|
||||
#define EXIT_JUMP(buf) longjmp(buf,1)
|
||||
#endif
|
||||
|
||||
#ifndef PROT_READ
|
||||
#define PROT_READ 1
|
||||
#define PROT_WRITE 2
|
||||
#define PROT_EXEC 4
|
||||
#endif
|
||||
|
||||
#define ERR(n) ((int32_t)(UINT32_C(0x80020000)|(n)))
|
||||
#define ATTR_MAGIC UINT32_C(0x41545452)
|
||||
#define STACK_MARGIN (256*1024)
|
||||
@@ -23,28 +40,7 @@
|
||||
#define DEFAULT_STACK (1024*1024)
|
||||
#define DEFAULT_PRIO 700
|
||||
|
||||
typedef void *(ABI *GuestEntry)(void *);
|
||||
typedef struct ThreadAttr {
|
||||
uint32_t magic;
|
||||
int detached, policy, prio, inherit;
|
||||
uint64_t stack, guard, affinity;
|
||||
struct ThreadAttr *next;
|
||||
} ThreadAttr;
|
||||
typedef struct GuestThread {
|
||||
/* Guest-visible TCB is allocated separately; this is the ScePthread handle. */
|
||||
uint64_t *tcb;
|
||||
unsigned char *tls_block;
|
||||
pthread_t host;
|
||||
GuestEntry entry;
|
||||
void *argument, *result;
|
||||
ThreadAttr attr;
|
||||
char name[32];
|
||||
int detached, finished, joined, host_owned;
|
||||
jmp_buf exit_jump;
|
||||
struct GuestThread *next;
|
||||
} GuestThread;
|
||||
|
||||
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
|
||||
static HostMutex lock = HOST_MUTEX_INIT;
|
||||
static ThreadAttr *attributes;
|
||||
static GuestThread *threads;
|
||||
static _Thread_local GuestThread *current;
|
||||
@@ -57,69 +53,113 @@ void runtime_set_main_tls(const void *data,uint64_t filesz,uint64_t memsz,uint64
|
||||
tls_template=data; tls_filesz=filesz; tls_memsz=memsz; tls_align=align ? align : 16;
|
||||
}
|
||||
static uint64_t tls_offset(void) { return (tls_memsz+tls_align-1)&~(tls_align-1); }
|
||||
static void set_gs(void *base) {
|
||||
|
||||
static void set_tls_base(void *base) {
|
||||
#ifdef _WIN32
|
||||
runtime_win_set_tcb(base);
|
||||
__asm__ __volatile__("wrfsbase %0" : : "r"(base));
|
||||
#else
|
||||
if (syscall(SYS_arch_prctl,ARCH_SET_GS,(unsigned long)base)) { perror("STOP: arch_prctl(ARCH_SET_GS)"); exit(21); }
|
||||
#endif
|
||||
}
|
||||
/* Build TCB/static TLS for the calling host thread and point GS at it. */
|
||||
|
||||
/* Build TCB/static TLS for the calling host thread and point GS (Linux) or FS/TEB (Windows) at it. */
|
||||
static void attach(GuestThread *t) {
|
||||
uint64_t offset=tls_offset();
|
||||
size_t total=offset+256;
|
||||
unsigned char *block=aligned_alloc(64,(total+63)&~(size_t)63);
|
||||
unsigned char *block=(unsigned char *)runtime_low_alloc_aligned((total+63)&~(size_t)63, 64);
|
||||
if (!block) { fputs("Cannot allocate guest TLS\n",stderr); exit(1); }
|
||||
CHECK_LOW_ADDR(block);
|
||||
memset(block,0,total);
|
||||
if (tls_filesz) memcpy(block,tls_template,tls_filesz);
|
||||
uint64_t *tcb=(uint64_t *)(block+offset);
|
||||
CHECK_LOW_ADDR(tcb);
|
||||
static uint64_t dtv[3];
|
||||
tcb[0]=(uint64_t)(uintptr_t)tcb; /* tcb_self */
|
||||
tcb[1]=(uint64_t)(uintptr_t)dtv; /* tcb_dtv (static module only) */
|
||||
tcb[2]=(uint64_t)(uintptr_t)t; /* tcb_thread */
|
||||
t->tls_block=block; t->tcb=tcb;
|
||||
set_gs(tcb);
|
||||
set_tls_base(tcb);
|
||||
#ifdef _WIN32
|
||||
if (!t->win32_handle) {
|
||||
DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &t->win32_handle, 0, FALSE, DUPLICATE_SAME_ACCESS);
|
||||
t->win32_tid = GetCurrentThreadId();
|
||||
}
|
||||
#endif
|
||||
current=t;
|
||||
}
|
||||
|
||||
static GuestThread *new_thread(void) {
|
||||
GuestThread *t=calloc(1,sizeof(*t));
|
||||
GuestThread *t=runtime_low_calloc(1,sizeof(*t));
|
||||
if (!t) return NULL;
|
||||
CHECK_LOW_ADDR(t);
|
||||
t->attr=(ThreadAttr){.magic=ATTR_MAGIC,.policy=1,.prio=DEFAULT_PRIO,.stack=DEFAULT_STACK,.affinity=0x7f};
|
||||
return t;
|
||||
}
|
||||
|
||||
static void publish(GuestThread *t) {
|
||||
pthread_mutex_lock(&lock); t->next=threads; threads=t; pthread_mutex_unlock(&lock);
|
||||
host_lock(&lock); t->next=threads; threads=t; host_unlock(&lock);
|
||||
}
|
||||
|
||||
/* Host threads that call into guest code without being created by the guest
|
||||
* (the main thread, test threads) get a record on first use. */
|
||||
GuestThread *runtime_thread_current(void) {
|
||||
if (current) return current;
|
||||
GuestThread *t=new_thread();
|
||||
if (!t) { fputs("Cannot allocate guest thread\n",stderr); exit(1); }
|
||||
t->host=pthread_self(); t->host_owned=1;
|
||||
#ifdef _WIN32
|
||||
t->host=GetCurrentThread();
|
||||
#else
|
||||
t->host=pthread_self();
|
||||
#endif
|
||||
t->host_owned=1;
|
||||
snprintf(t->name,sizeof(t->name),"host");
|
||||
attach(t); publish(t);
|
||||
return t;
|
||||
}
|
||||
/* Host threads that call guest code (HLE decoders invoking guest callbacks) need
|
||||
* a TCB/TLS like guest threads; the record lives until process exit. */
|
||||
|
||||
void runtime_thread_attach_host(const char *name) {
|
||||
GuestThread *t=runtime_thread_current();
|
||||
snprintf(t->name,sizeof(t->name),"%s",name ? name : "host");
|
||||
}
|
||||
|
||||
void runtime_thread_attach_main(void) {
|
||||
#ifdef _WIN32
|
||||
static int s_process_prio_configured = 0;
|
||||
if (!s_process_prio_configured) {
|
||||
s_process_prio_configured = 1;
|
||||
SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS);
|
||||
PROCESS_POWER_THROTTLING_STATE throttling = {0};
|
||||
throttling.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
|
||||
throttling.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED;
|
||||
throttling.StateMask = 0;
|
||||
SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &throttling, sizeof(throttling));
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
|
||||
}
|
||||
#endif
|
||||
GuestThread *t=runtime_thread_current();
|
||||
snprintf(t->name,sizeof(t->name),"main");
|
||||
}
|
||||
|
||||
void *runtime_thread_get_tcb(void) {
|
||||
GuestThread *t=runtime_thread_current();
|
||||
return t ? t->tcb : NULL;
|
||||
}
|
||||
|
||||
static GuestThread *find_thread(void *handle) {
|
||||
GuestThread *found=NULL;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
for (GuestThread *t=threads;t;t=t->next) if (t==handle) { found=t; break; }
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return found;
|
||||
}
|
||||
|
||||
static ThreadAttr *find_attr(ThreadAttr **slot) {
|
||||
if (!slot || !*slot) return NULL;
|
||||
ThreadAttr *found=NULL;
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
for (ThreadAttr *a=attributes;a;a=a->next) if (a==*slot && a->magic==ATTR_MAGIC) { found=a; break; }
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -129,22 +169,24 @@ int32_t *runtime_errno(void) { return &guest_errno; }
|
||||
|
||||
static ABI int32_t attr_init(ThreadAttr **out) {
|
||||
if (!out) return ERR(22);
|
||||
ThreadAttr *a=calloc(1,sizeof(*a));
|
||||
ThreadAttr *a=runtime_low_calloc(1,sizeof(*a));
|
||||
if (!a) return ERR(12);
|
||||
*a=(ThreadAttr){.magic=ATTR_MAGIC,.policy=1,.prio=DEFAULT_PRIO,.inherit=4,.stack=DEFAULT_STACK,.guard=4096,.affinity=0x7f};
|
||||
pthread_mutex_lock(&lock); a->next=attributes; attributes=a; pthread_mutex_unlock(&lock);
|
||||
host_lock(&lock); a->next=attributes; attributes=a; host_unlock(&lock);
|
||||
*out=a; return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t attr_destroy(ThreadAttr **slot) {
|
||||
ThreadAttr *a=find_attr(slot);
|
||||
if (!a) return ERR(22);
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
ThreadAttr **link=&attributes;
|
||||
while (*link!=a) link=&(*link)->next;
|
||||
*link=a->next; a->magic=0;
|
||||
pthread_mutex_unlock(&lock);
|
||||
free(a); *slot=NULL; return 0;
|
||||
host_unlock(&lock);
|
||||
runtime_low_free(a); *slot=NULL; return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t attr_get(void *thread,ThreadAttr **out) {
|
||||
ThreadAttr *a=find_attr(out);
|
||||
if (!a || !thread) return ERR(22);
|
||||
@@ -155,6 +197,7 @@ static ABI int32_t attr_get(void *thread,ThreadAttr **out) {
|
||||
a->detached=t->detached;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t attr_set_stack(ThreadAttr **slot,uint64_t size) {
|
||||
ThreadAttr *a=find_attr(slot);
|
||||
if (!a || size<16384) return ERR(22);
|
||||
@@ -211,21 +254,77 @@ static ABI int32_t attr_set_guard(ThreadAttr **slot,uint64_t size) {
|
||||
a->guard=size; return 0;
|
||||
}
|
||||
|
||||
/* Linux thread names hold 15 characters; longer ones would be rejected. */
|
||||
static void set_host_name(const char *name) {
|
||||
char host[16]={0};
|
||||
memcpy(host,name,strnlen(name,sizeof(host)-1));
|
||||
#ifdef _WIN32
|
||||
runtime_win_set_thread_name(host);
|
||||
#else
|
||||
pthread_setname_np(pthread_self(),host);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static int get_host_cores(void) {
|
||||
static int s_cores = 0;
|
||||
if (!s_cores) {
|
||||
SYSTEM_INFO si;
|
||||
GetSystemInfo(&si);
|
||||
s_cores = (int)si.dwNumberOfProcessors;
|
||||
if (s_cores < 1) s_cores = 1;
|
||||
}
|
||||
return s_cores;
|
||||
}
|
||||
|
||||
static int ps4_prio_to_win32(int prio, const char *name) {
|
||||
int cores = get_host_cores();
|
||||
int is_audio = (name && (strstr(name, "Audio") || strstr(name, "audio") || strstr(name, "Sound") || strstr(name, "sound") || strstr(name, "pad")));
|
||||
int is_render = (name && (strstr(name, "Render") || strstr(name, "render") || strstr(name, "Present") || strstr(name, "Flip") || strstr(name, "Display")));
|
||||
int is_main = (name && (strstr(name, "Main") || strstr(name, "main") || strstr(name, "host")));
|
||||
int is_worker = (name && (strstr(name, "Worker") || strstr(name, "worker") || strstr(name, "Pool") || strstr(name, "pool") || strstr(name, "Work") || strstr(name, "work") || strstr(name, "Havok") || strstr(name, "Cloth") || strstr(name, "Job") || strstr(name, "job")));
|
||||
|
||||
/* Audio threads get elevated priority to prevent audio dropouts */
|
||||
if (is_audio || prio <= 180) {
|
||||
return THREAD_PRIORITY_ABOVE_NORMAL;
|
||||
}
|
||||
|
||||
/* Rendering and main display threads are on the critical display/GPU submission path */
|
||||
if (is_render || is_main) {
|
||||
return THREAD_PRIORITY_ABOVE_NORMAL;
|
||||
}
|
||||
|
||||
/* On 4 cores or fewer (no SMT / low core count), worker pools must NOT preempt main or render threads */
|
||||
if (cores <= 4) {
|
||||
if (is_worker) return THREAD_PRIORITY_BELOW_NORMAL;
|
||||
if (prio <= 260) return THREAD_PRIORITY_BELOW_NORMAL;
|
||||
if (prio <= 700) return THREAD_PRIORITY_BELOW_NORMAL;
|
||||
return THREAD_PRIORITY_LOWEST;
|
||||
}
|
||||
|
||||
/* Higher core systems (6C/12T+) */
|
||||
if (is_worker) return THREAD_PRIORITY_BELOW_NORMAL;
|
||||
if (prio <= 260) return THREAD_PRIORITY_ABOVE_NORMAL;
|
||||
if (prio <= 400) return THREAD_PRIORITY_NORMAL;
|
||||
if (prio <= 700) return THREAD_PRIORITY_BELOW_NORMAL;
|
||||
return THREAD_PRIORITY_LOWEST;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void *host_start(void *p) {
|
||||
GuestThread *t=p;
|
||||
attach(t);
|
||||
set_host_name(t->name);
|
||||
if (!setjmp(t->exit_jump)) t->result=t->entry(t->argument);
|
||||
#ifdef _WIN32
|
||||
if (t->attr.prio > 0) {
|
||||
SetThreadPriority(GetCurrentThread(), ps4_prio_to_win32(t->attr.prio, t->name));
|
||||
}
|
||||
#endif
|
||||
if (!EXIT_SET(t->exit_jump)) t->result=t->entry(t->argument);
|
||||
runtime_thread_keys_cleanup();
|
||||
pthread_mutex_lock(&lock); t->finished=1; ++exited; pthread_mutex_unlock(&lock);
|
||||
host_lock(&lock); t->finished=1; ++exited; host_unlock(&lock);
|
||||
return t->result;
|
||||
}
|
||||
|
||||
static int32_t create(GuestThread **out,ThreadAttr **attr_slot,GuestEntry entry,void *argument,const char *name) {
|
||||
if (!out || !entry) return ERR(22);
|
||||
ThreadAttr *a=NULL;
|
||||
@@ -235,64 +334,88 @@ static int32_t create(GuestThread **out,ThreadAttr **attr_slot,GuestEntry entry,
|
||||
if (a) { t->attr=*a; t->attr.next=NULL; }
|
||||
t->entry=entry; t->argument=argument; t->detached=t->attr.detached;
|
||||
snprintf(t->name,sizeof(t->name),"%s",name ? name : "guest");
|
||||
uint64_t stack=t->attr.stack<MIN_STACK ? MIN_STACK : t->attr.stack;
|
||||
size_t stack_bytes=(size_t)stack+STACK_MARGIN;
|
||||
publish(t);
|
||||
CHECK_LOW_ADDR(t);
|
||||
*out=t;
|
||||
#ifdef _WIN32
|
||||
int e=host_thread_start(&t->host,stack_bytes,host_start,t);
|
||||
#else
|
||||
pthread_attr_t host;
|
||||
pthread_attr_init(&host);
|
||||
uint64_t stack=t->attr.stack<MIN_STACK ? MIN_STACK : t->attr.stack;
|
||||
/* Stacks below 1 TiB as on PS4; guest code may pack stack addresses. */
|
||||
size_t stack_bytes=(size_t)stack+STACK_MARGIN;
|
||||
void *stack_memory=runtime_low_map(stack_bytes,PROT_READ|PROT_WRITE);
|
||||
if (stack_memory) pthread_attr_setstack(&host,stack_memory,stack_bytes);
|
||||
else pthread_attr_setstacksize(&host,stack_bytes);
|
||||
publish(t);
|
||||
/* Publish the handle before the thread can run and inspect itself. */
|
||||
*out=t;
|
||||
if (stack_memory) {
|
||||
CHECK_LOW_ADDR(stack_memory);
|
||||
pthread_attr_setstack(&host,stack_memory,stack_bytes);
|
||||
} else {
|
||||
pthread_attr_setstacksize(&host,stack_bytes);
|
||||
}
|
||||
int e=pthread_create(&t->host,&host,host_start,t);
|
||||
pthread_attr_destroy(&host);
|
||||
if (e) { fprintf(stderr,"STOP: host pthread_create failed: %d\n",e); exit(21); }
|
||||
if (t->detached) pthread_detach(t->host);
|
||||
pthread_mutex_lock(&lock); ++created; pthread_mutex_unlock(&lock);
|
||||
#endif
|
||||
if (e) {
|
||||
fprintf(stderr,"STOP: host thread creation failed: %d\n",e);
|
||||
return ERR(35);
|
||||
}
|
||||
if (t->detached) host_thread_detach(t->host);
|
||||
host_lock(&lock); ++created; host_unlock(&lock);
|
||||
printf("Runtime: guest thread '%s' created (stack=%llu, prio=%d)\n",t->name,(unsigned long long)stack,t->attr.prio);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t thread_create(GuestThread **out,ThreadAttr **attr,GuestEntry entry,void *argument,const char *name) {
|
||||
return create(out,attr,entry,argument,name);
|
||||
}
|
||||
|
||||
static ABI int32_t thread_join(GuestThread *t,void **result) {
|
||||
if (!find_thread(t) || t->host_owned) return ERR(3);
|
||||
if (t==current) return ERR(11);
|
||||
if (t->detached || t->joined) return ERR(22);
|
||||
t->joined=1;
|
||||
void *value=NULL;
|
||||
int e=pthread_join(t->host,&value);
|
||||
if (e) { fprintf(stderr,"STOP: host pthread_join failed: %d\n",e); exit(21); }
|
||||
runtime_thread_set_blocked("thread_join", (uintptr_t)t);
|
||||
int e=host_thread_join(t->host);
|
||||
runtime_thread_clear_blocked();
|
||||
if (e) { fprintf(stderr,"STOP: host thread join failed: %d\n",e); exit(21); }
|
||||
if (result) *result=t->result;
|
||||
pthread_mutex_lock(&lock); ++joined_count; pthread_mutex_unlock(&lock);
|
||||
host_lock(&lock); ++joined_count; host_unlock(&lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t thread_detach(GuestThread *t) {
|
||||
if (!find_thread(t)) return ERR(3);
|
||||
if (t->detached) return ERR(22);
|
||||
t->detached=1;
|
||||
if (!t->host_owned) pthread_detach(t->host);
|
||||
if (!t->host_owned) host_thread_detach(t->host);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ABI __attribute__((noreturn)) void thread_exit(void *value) {
|
||||
GuestThread *t=runtime_thread_current();
|
||||
if (t->host_owned) { fputs("STOP: pthread_exit on host-owned/main thread\n",stderr); exit(21); }
|
||||
t->result=value;
|
||||
/* No host unwinder: guest frames have no registered FDEs. */
|
||||
longjmp(t->exit_jump,1);
|
||||
EXIT_JUMP(t->exit_jump);
|
||||
}
|
||||
static ABI int32_t thread_yield(void) { sched_yield(); return 0; }
|
||||
|
||||
static ABI int32_t thread_yield(void) { host_yield(); return 0; }
|
||||
|
||||
static ABI int32_t thread_get_prio(GuestThread *t,int *prio) {
|
||||
if (!find_thread(t)) return ERR(3);
|
||||
if (!prio) return ERR(22);
|
||||
*prio=t->attr.prio; return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t thread_set_prio(GuestThread *t,int prio) {
|
||||
if (!find_thread(t)) return ERR(3);
|
||||
t->attr.prio=prio; return 0;
|
||||
t->attr.prio=prio;
|
||||
#ifdef _WIN32
|
||||
if (t->win32_handle) {
|
||||
SetThreadPriority(t->win32_handle, ps4_prio_to_win32(prio, t->name));
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ABI int32_t thread_set_affinity(GuestThread *t,uint64_t mask) {
|
||||
if (!find_thread(t)) return ERR(3);
|
||||
t->attr.affinity=mask; return 0;
|
||||
@@ -307,6 +430,11 @@ static ABI int32_t thread_rename(GuestThread *t,const char *name) {
|
||||
if (!name) return ERR(22);
|
||||
snprintf(t->name,sizeof(t->name),"%s",name);
|
||||
if (t==current) set_host_name(t->name);
|
||||
#ifdef _WIN32
|
||||
if (t->win32_handle && t->attr.prio > 0) {
|
||||
SetThreadPriority(t->win32_handle, ps4_prio_to_win32(t->attr.prio, t->name));
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
static ABI int32_t thread_equal(GuestThread *a,GuestThread *b) { return a==b; }
|
||||
@@ -348,14 +476,41 @@ uintptr_t runtime_thread_resolve(const char *name) {
|
||||
if (!strcmp(name,table[i].nid)) return (uintptr_t)table[i].fn;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void runtime_thread_report(void) {
|
||||
pthread_mutex_lock(&lock);
|
||||
host_lock(&lock);
|
||||
printf("Runtime: guest threads created=%zu, exited=%zu, joined=%zu\n",created,exited,joined_count);
|
||||
pthread_mutex_unlock(&lock);
|
||||
host_unlock(&lock);
|
||||
}
|
||||
|
||||
GuestThread *runtime_thread_get_all(void) {
|
||||
return threads;
|
||||
}
|
||||
|
||||
void *runtime_thread_get_lock(void) {
|
||||
return (void *)&lock;
|
||||
}
|
||||
|
||||
void runtime_thread_set_blocked(const char *what, uint64_t resource) {
|
||||
GuestThread *t = current;
|
||||
if (t) {
|
||||
t->blocked_on = what;
|
||||
t->blocked_resource = resource;
|
||||
#ifdef _WIN32
|
||||
t->blocked_tick = GetTickCount64();
|
||||
#else
|
||||
uintptr_t runtime_thread_resolve(const char *name) { (void)name; return 0; }
|
||||
void runtime_thread_report(void) {}
|
||||
void runtime_thread_attach_main(void) {}
|
||||
void runtime_set_main_tls(const void *d,uint64_t f,uint64_t m,uint64_t a) { (void)d;(void)f;(void)m;(void)a; }
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
t->blocked_tick = (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void runtime_thread_clear_blocked(void) {
|
||||
GuestThread *t = current;
|
||||
if (t) {
|
||||
t->blocked_on = NULL;
|
||||
t->blocked_resource = 0;
|
||||
t->blocked_tick = 0;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <sys/time.h>
|
||||
typedef struct { int64_t seconds, microseconds; } GuestTimeval;
|
||||
typedef struct { int32_t minuteswest, dsttime; } GuestTimezone;
|
||||
@@ -23,6 +22,3 @@ uintptr_t runtime_time_resolve(const char *name) {
|
||||
if (!strcmp(name,"n88vx3C5nW8#I#J")) return (uintptr_t)guest_gettimeofday;
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
uintptr_t runtime_time_resolve(const char *name) { (void)name; return 0; }
|
||||
#endif
|
||||
@@ -0,0 +1,151 @@
|
||||
/* Windows implementations of POSIX pieces declared in win32_compat.h. */
|
||||
#ifdef _WIN32
|
||||
#define _CRT_RAND_S
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <io.h>
|
||||
#include <direct.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include "win32_compat.h"
|
||||
|
||||
int runtime_win_mkdir(const char *path, int mode) { (void)mode; return _mkdir(path); }
|
||||
|
||||
int runtime_win_rename(const char *from, const char *to) {
|
||||
if (MoveFileExA(from, to, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED)) return 0;
|
||||
DWORD e = GetLastError();
|
||||
errno = e == ERROR_FILE_NOT_FOUND || e == ERROR_PATH_NOT_FOUND ? ENOENT
|
||||
: e == ERROR_ACCESS_DENIED || e == ERROR_SHARING_VIOLATION ? EACCES
|
||||
: e == ERROR_DIR_NOT_EMPTY ? ENOTEMPTY : EIO;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int64_t positional(int fd, void *buffer, size_t size, int64_t offset, int write) {
|
||||
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
||||
if (h == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; }
|
||||
if (offset < 0) { errno = EINVAL; return -1; }
|
||||
LARGE_INTEGER zero = {0}, position;
|
||||
if (!SetFilePointerEx(h, zero, &position, FILE_CURRENT)) { errno = ESPIPE; return -1; }
|
||||
int64_t done = 0;
|
||||
while ((size_t)done < size) {
|
||||
DWORD chunk = size - (size_t)done > 0x40000000u ? 0x40000000u : (DWORD)(size - (size_t)done), n = 0;
|
||||
OVERLAPPED at = {0};
|
||||
at.Offset = (DWORD)(uint64_t)(offset + done);
|
||||
at.OffsetHigh = (DWORD)((uint64_t)(offset + done) >> 32);
|
||||
BOOL ok = write ? WriteFile(h, (const char *)buffer + done, chunk, &n, &at)
|
||||
: ReadFile(h, (char *)buffer + done, chunk, &n, &at);
|
||||
if (!ok) {
|
||||
DWORD e = GetLastError();
|
||||
if (e == ERROR_HANDLE_EOF) break;
|
||||
SetFilePointerEx(h, position, NULL, FILE_BEGIN);
|
||||
if (done) return done;
|
||||
errno = e == ERROR_NOACCESS ? EFAULT : e == ERROR_ACCESS_DENIED ? EBADF : EIO;
|
||||
return -1;
|
||||
}
|
||||
done += n;
|
||||
if (n < chunk) break;
|
||||
}
|
||||
SetFilePointerEx(h, position, NULL, FILE_BEGIN);
|
||||
return done;
|
||||
}
|
||||
|
||||
int64_t runtime_win_pread(int fd, void *buffer, size_t size, int64_t offset) {
|
||||
return positional(fd, buffer, size, offset, 0);
|
||||
}
|
||||
|
||||
int64_t runtime_win_pwrite(int fd, const void *buffer, size_t size, int64_t offset) {
|
||||
return positional(fd, (void *)buffer, size, offset, 1);
|
||||
}
|
||||
|
||||
int runtime_win_remove_tree(const char *path) {
|
||||
struct _stat64 s;
|
||||
if (_stat64(path, &s)) return -1;
|
||||
if (!(s.st_mode & _S_IFDIR)) return remove(path);
|
||||
DIR *d = opendir(path);
|
||||
if (d) {
|
||||
for (struct dirent *e; (e = readdir(d));) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
char child[2048];
|
||||
snprintf(child, sizeof(child), "%s/%s", path, e->d_name);
|
||||
runtime_win_remove_tree(child);
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
return _rmdir(path);
|
||||
}
|
||||
|
||||
int64_t runtime_utc_offset(time_t when) {
|
||||
struct tm local;
|
||||
if (localtime_s(&local, &when)) return 0;
|
||||
return (int64_t)(_mkgmtime(&local) - when);
|
||||
}
|
||||
|
||||
static int64_t filetime_us(FILETIME t) {
|
||||
return (int64_t)((((uint64_t)t.dwHighDateTime << 32) | t.dwLowDateTime) / 10);
|
||||
}
|
||||
|
||||
void runtime_cpu_times(int thread, int64_t *user_us, int64_t *system_us) {
|
||||
FILETIME created, exited, kernel, user;
|
||||
BOOL ok = thread ? GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user)
|
||||
: GetProcessTimes(GetCurrentProcess(), &created, &exited, &kernel, &user);
|
||||
*user_us = ok ? filetime_us(user) : 0;
|
||||
*system_us = ok ? filetime_us(kernel) : 0;
|
||||
}
|
||||
|
||||
int runtime_random(void *buffer, size_t size) {
|
||||
unsigned char *out = buffer;
|
||||
for (size_t i = 0; i < size; i += 4) {
|
||||
unsigned int value;
|
||||
if (rand_s(&value)) { errno = EIO; return -1; }
|
||||
memcpy(out + i, &value, size - i < 4 ? size - i : 4);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static DWORD tls_slot = TLS_OUT_OF_INDEXES;
|
||||
static INIT_ONCE tls_once = INIT_ONCE_STATIC_INIT;
|
||||
static BOOL CALLBACK allocate_slot(PINIT_ONCE once, PVOID parameter, PVOID *context) {
|
||||
(void)once; (void)parameter; (void)context;
|
||||
tls_slot = TlsAlloc();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
uint32_t runtime_win_tls_slot(void) {
|
||||
InitOnceExecuteOnce(&tls_once, allocate_slot, NULL, NULL);
|
||||
if (tls_slot == TLS_OUT_OF_INDEXES) {
|
||||
fputs("STOP: no TLS slot for the guest thread pointer\n", stderr);
|
||||
exit(21);
|
||||
}
|
||||
return tls_slot;
|
||||
}
|
||||
|
||||
void runtime_win_set_tcb(void *tcb) { TlsSetValue(runtime_win_tls_slot(), tcb); }
|
||||
|
||||
void runtime_win_set_thread_name(const char *name) {
|
||||
wchar_t wide[64];
|
||||
if (MultiByteToWideChar(CP_UTF8, 0, name, -1, wide, 64)) {
|
||||
typedef HRESULT (WINAPI *SetThreadDescFn)(HANDLE, PCWSTR);
|
||||
static SetThreadDescFn set_desc = NULL;
|
||||
static int checked = 0;
|
||||
if (!checked) {
|
||||
HMODULE kb = GetModuleHandleW(L"kernelbase.dll");
|
||||
if (kb) set_desc = (SetThreadDescFn)(void *)GetProcAddress(kb, "SetThreadDescription");
|
||||
checked = 1;
|
||||
}
|
||||
if (set_desc) set_desc(GetCurrentThread(), wide);
|
||||
}
|
||||
}
|
||||
|
||||
int runtime_win_inet_pton4(const char *src, void *dst) { return inet_pton(AF_INET, src, dst); }
|
||||
const char *runtime_win_inet_ntop4(const void *src, char *dst, uint32_t size) {
|
||||
return inet_ntop(AF_INET, src, dst, size);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
/* Windows host: file system, time zone, CPU time, randomness and naming pieces. */
|
||||
#ifndef BB_WIN32_COMPAT_H
|
||||
#define BB_WIN32_COMPAT_H
|
||||
#ifdef _WIN32
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <direct.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#define PROT_NONE 0
|
||||
#define PROT_READ 1
|
||||
#define PROT_WRITE 2
|
||||
#define PROT_EXEC 4
|
||||
#ifndef O_NONBLOCK
|
||||
#define O_NONBLOCK 0
|
||||
#endif
|
||||
#ifndef O_SYNC
|
||||
#define O_SYNC 0
|
||||
#endif
|
||||
#ifndef O_DIRECTORY
|
||||
#define O_DIRECTORY 0
|
||||
#endif
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC _O_NOINHERIT
|
||||
#endif
|
||||
#ifndef PATH_MAX
|
||||
#define PATH_MAX 1024
|
||||
#endif
|
||||
|
||||
/* POSIX calls with a Windows implementation. */
|
||||
#define mkdir(path, mode) runtime_win_mkdir(path, mode)
|
||||
#define rename(from, to) runtime_win_rename(from, to)
|
||||
#define fsync(fd) _commit(fd)
|
||||
#define realpath(path, resolved) _fullpath(resolved, path, PATH_MAX)
|
||||
#define pread runtime_win_pread
|
||||
#define pwrite runtime_win_pwrite
|
||||
int runtime_win_mkdir(const char *path, int mode);
|
||||
int runtime_win_rename(const char *from, const char *to);
|
||||
int64_t runtime_win_pread(int fd, void *buffer, size_t size, int64_t offset);
|
||||
int64_t runtime_win_pwrite(int fd, const void *buffer, size_t size, int64_t offset);
|
||||
int runtime_win_remove_tree(const char *path);
|
||||
|
||||
/* Time: local UTC offset, CPU times, randomness. */
|
||||
int64_t runtime_utc_offset(time_t when);
|
||||
void runtime_cpu_times(int thread, int64_t *user_us, int64_t *system_us);
|
||||
int runtime_random(void *buffer, size_t size);
|
||||
|
||||
/* Threads: guest TCB in a TEB TLS slot. */
|
||||
uint32_t runtime_win_tls_slot(void);
|
||||
void runtime_win_set_tcb(void *tcb);
|
||||
void runtime_win_set_thread_name(const char *name);
|
||||
|
||||
/* Networking helpers without winsock headers. */
|
||||
int runtime_win_inet_pton4(const char *src, void *dst);
|
||||
const char *runtime_win_inet_ntop4(const void *src, char *dst, uint32_t size);
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,247 @@
|
||||
/* Windows guest address space manager.
|
||||
* Reserves the guest address range at startup as placeholders (VirtualAlloc2).
|
||||
* Mappings replace placeholders with views of the physical backing section (MapViewOfFile3).
|
||||
* Slices are tracked so partial unmap / overlay unmaps and remaps remnants. */
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "win32_memory.h"
|
||||
|
||||
typedef PVOID (WINAPI *VirtualAlloc2Fn)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER *, ULONG);
|
||||
typedef PVOID (WINAPI *MapViewOfFile3Fn)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER *, ULONG);
|
||||
typedef BOOL (WINAPI *UnmapViewOfFile2Fn)(HANDLE, PVOID, ULONG);
|
||||
|
||||
static VirtualAlloc2Fn virtual_alloc2;
|
||||
static MapViewOfFile3Fn map_view3;
|
||||
static UnmapViewOfFile2Fn unmap_view2;
|
||||
static HANDLE section;
|
||||
static void *section_backing;
|
||||
static uintptr_t space_start, space_end;
|
||||
|
||||
typedef struct { uintptr_t start, end; uint64_t phys; } View;
|
||||
static View *views;
|
||||
static size_t view_count, view_capacity;
|
||||
|
||||
static void report(const char *what, uintptr_t a, uintptr_t b) {
|
||||
fprintf(stderr, "Runtime: %s [%#llx, %#llx) failed: Windows error %lu\n", what,
|
||||
(unsigned long long)a, (unsigned long long)b, GetLastError());
|
||||
}
|
||||
|
||||
int win_mem_space(uintptr_t start, uintptr_t end) {
|
||||
if (space_end) return 0;
|
||||
HMODULE kernel = GetModuleHandleW(L"kernelbase.dll");
|
||||
if (!kernel) kernel = GetModuleHandleW(L"kernel32.dll");
|
||||
virtual_alloc2 = kernel ? (VirtualAlloc2Fn)(void *)GetProcAddress(kernel, "VirtualAlloc2") : NULL;
|
||||
map_view3 = kernel ? (MapViewOfFile3Fn)(void *)GetProcAddress(kernel, "MapViewOfFile3") : NULL;
|
||||
unmap_view2 = kernel ? (UnmapViewOfFile2Fn)(void *)GetProcAddress(kernel, "UnmapViewOfFile2") : NULL;
|
||||
if (!virtual_alloc2 || !map_view3 || !unmap_view2) {
|
||||
/* Fallback for pre-1803 systems */
|
||||
void *res = VirtualAlloc((void *)start, end - start, MEM_RESERVE, PAGE_NOACCESS);
|
||||
if (res != (void *)start) {
|
||||
report("reserving fallback guest address space", start, end);
|
||||
return -1;
|
||||
}
|
||||
space_start = start; space_end = end;
|
||||
return 0;
|
||||
}
|
||||
if (virtual_alloc2(GetCurrentProcess(), (void *)start, end - start, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER,
|
||||
PAGE_NOACCESS, NULL, 0) == (void *)start) {
|
||||
space_start = start; space_end = end;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Fallback: if single placeholder reservation fails (e.g. preexisting thread stack or module),
|
||||
* query memory and reserve all free regions in [start, end) as placeholders. */
|
||||
MEMORY_BASIC_INFORMATION info;
|
||||
uintptr_t at = start;
|
||||
int reserved_chunks = 0;
|
||||
while (at < end) {
|
||||
if (!VirtualQuery((void *)at, &info, sizeof(info))) break;
|
||||
uintptr_t chunk_start = (uintptr_t)info.BaseAddress;
|
||||
uintptr_t chunk_end = chunk_start + info.RegionSize;
|
||||
if (chunk_start < start) chunk_start = start;
|
||||
if (chunk_end > end) chunk_end = end;
|
||||
|
||||
if (info.State == MEM_FREE) {
|
||||
SIZE_T chunk_size = chunk_end - chunk_start;
|
||||
void *p = virtual_alloc2(GetCurrentProcess(), (void *)chunk_start, chunk_size,
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, NULL, 0);
|
||||
if (p == (void *)chunk_start) reserved_chunks++;
|
||||
}
|
||||
if (chunk_end <= at) break;
|
||||
at = chunk_end;
|
||||
}
|
||||
if (reserved_chunks > 0) {
|
||||
space_start = start; space_end = end;
|
||||
return 0;
|
||||
}
|
||||
report("reserving the guest address space placeholder", start, end);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int win_mem_section(uint64_t size, void **backing) {
|
||||
section = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE | SEC_COMMIT,
|
||||
(DWORD)(size >> 32), (DWORD)size, NULL);
|
||||
if (!section) { report("creating the memory pool section", 0, size); return -1; }
|
||||
section_backing = MapViewOfFile(section, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||
if (!section_backing) { report("mapping the memory pool", 0, size); return -1; }
|
||||
*backing = section_backing;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static DWORD page_mode(int prot) {
|
||||
switch (prot & 7) {
|
||||
case 0: return PAGE_NOACCESS;
|
||||
case 1: return PAGE_READONLY;
|
||||
case 2: case 3: return PAGE_READWRITE;
|
||||
case 4: return PAGE_EXECUTE;
|
||||
case 5: return PAGE_EXECUTE_READ;
|
||||
default: return PAGE_EXECUTE_READWRITE;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t view_index(uintptr_t a) {
|
||||
size_t lo = 0, hi = view_count;
|
||||
while (lo < hi) { size_t mid = (lo + hi) / 2; if (views[mid].end <= a) lo = mid + 1; else hi = mid; }
|
||||
return lo;
|
||||
}
|
||||
|
||||
static int view_insert(View v) {
|
||||
if (view_count == view_capacity) {
|
||||
size_t capacity = view_capacity ? view_capacity * 2 : 256;
|
||||
View *next = realloc(views, capacity * sizeof(*views));
|
||||
if (!next) return -1;
|
||||
views = next; view_capacity = capacity;
|
||||
}
|
||||
size_t at = view_index(v.start);
|
||||
memmove(views + at + 1, views + at, (view_count - at) * sizeof(*views));
|
||||
views[at] = v; ++view_count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void view_erase(size_t at) {
|
||||
memmove(views + at, views + at + 1, (view_count - at - 1) * sizeof(*views));
|
||||
--view_count;
|
||||
}
|
||||
|
||||
static int split_at(uintptr_t x) {
|
||||
if (x == space_start || x == space_end) return 0;
|
||||
MEMORY_BASIC_INFORMATION info;
|
||||
if (!VirtualQuery((void *)x, &info, sizeof(info))) return -1;
|
||||
if ((uintptr_t)info.AllocationBase == x) return 0;
|
||||
if (info.State != MEM_RESERVE) { SetLastError(ERROR_INVALID_ADDRESS); return -1; }
|
||||
return VirtualFree((void *)x, info.RegionSize, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int make_placeholder(uintptr_t a, uintptr_t b) {
|
||||
if (split_at(a) || split_at(b)) return -1;
|
||||
MEMORY_BASIC_INFORMATION info;
|
||||
if (!VirtualQuery((void *)a, &info, sizeof(info))) return -1;
|
||||
if (info.RegionSize < b - a && !VirtualFree((void *)a, b - a, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int map_view(uintptr_t a, uintptr_t b, uint64_t phys) {
|
||||
if (make_placeholder(a, b)) return -1;
|
||||
if (map_view3(section, GetCurrentProcess(), (void *)a, phys, b - a, MEM_REPLACE_PLACEHOLDER,
|
||||
PAGE_READWRITE, NULL, 0) != (void *)a) return -1;
|
||||
return view_insert((View){a, b, phys});
|
||||
}
|
||||
|
||||
static int inside(uintptr_t a, uintptr_t b) { return a < b && a >= space_start && b <= space_end; }
|
||||
|
||||
int win_mem_release(uintptr_t a, uintptr_t b, WinMemRestore restore) {
|
||||
if (!inside(a, b)) { SetLastError(ERROR_INVALID_ADDRESS); report("releasing", a, b); return -1; }
|
||||
for (size_t i = view_index(a); i < view_count && views[i].start < b;) {
|
||||
View v = views[i];
|
||||
if (!unmap_view2(GetCurrentProcess(), (void *)v.start, MEM_PRESERVE_PLACEHOLDER)) {
|
||||
report("unmapping", v.start, v.end);
|
||||
return -1;
|
||||
}
|
||||
view_erase(i);
|
||||
if (v.start < a) {
|
||||
if (map_view(v.start, a, v.phys)) { report("remapping remnant prefix", v.start, a); return -1; }
|
||||
if (restore) restore(v.start, a);
|
||||
++i;
|
||||
}
|
||||
if (v.end > b) {
|
||||
if (map_view(b, v.end, v.phys + (b - v.start))) { report("remapping remnant suffix", b, v.end); return -1; }
|
||||
if (restore) restore(b, v.end);
|
||||
}
|
||||
}
|
||||
if (make_placeholder(a, b)) { report("reserving placeholder", a, b); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int win_mem_map(uintptr_t a, uintptr_t b, uint64_t phys, int prot, WinMemRestore restore) {
|
||||
if (win_mem_release(a, b, restore)) return -1;
|
||||
if (map_view(a, b, phys)) { report("mapping view", a, b); return -1; }
|
||||
return (prot & 7) == 3 ? 0 : win_mem_protect(a, b, prot);
|
||||
}
|
||||
|
||||
int win_mem_protect(uintptr_t a, uintptr_t b, int prot) {
|
||||
DWORD mode = page_mode(prot), old;
|
||||
for (size_t i = view_index(a); i < view_count && views[i].start < b; ++i) {
|
||||
uintptr_t start = views[i].start > a ? views[i].start : a;
|
||||
uintptr_t end = views[i].end < b ? views[i].end : b;
|
||||
if (!VirtualProtect((void *)start, end - start, mode, &old)) {
|
||||
report("protecting view slice", start, end);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *win_mem_private(uintptr_t a, uintptr_t b) {
|
||||
if (!inside(a, b)) return NULL;
|
||||
MEMORY_BASIC_INFORMATION info;
|
||||
for (uintptr_t at = a; at < b; at = (uintptr_t)info.BaseAddress + info.RegionSize)
|
||||
if (!VirtualQuery((void *)at, &info, sizeof(info)) || info.State != MEM_RESERVE) return NULL;
|
||||
if (make_placeholder(a, b)) return NULL;
|
||||
return virtual_alloc2(GetCurrentProcess(), (void *)a, b - a, MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER,
|
||||
PAGE_READWRITE, NULL, 0);
|
||||
}
|
||||
|
||||
/* Lazy punch/discard: zero or reset pages in backing section without blocking memset */
|
||||
int win_mem_punch(uint64_t phys, uint64_t size) {
|
||||
if (!section_backing) return 0;
|
||||
void *ptr = (char *)section_backing + phys;
|
||||
/* MEM_RESET notifies Windows memory manager that the pages are discarded and can be zeroed lazily */
|
||||
VirtualAlloc(ptr, size, MEM_RESET, PAGE_READWRITE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void win_mem_run_selftest(void) {
|
||||
printf("[SELFTEST] Running win32_memory validation...\n");
|
||||
const uintptr_t test_addr = space_start + 0x1000000;
|
||||
const uint64_t test_size = 64 * 1024;
|
||||
int r = win_mem_map(test_addr, test_addr + test_size, 0, 3, NULL);
|
||||
if (r != 0) {
|
||||
printf("[SELFTEST] FAIL: initial map failed\n");
|
||||
return;
|
||||
}
|
||||
volatile uint32_t *data = (volatile uint32_t *)test_addr;
|
||||
data[0] = 0x12345678;
|
||||
data[test_size / 4 - 1] = 0x87654321;
|
||||
if (data[0] != 0x12345678 || data[test_size / 4 - 1] != 0x87654321) {
|
||||
printf("[SELFTEST] FAIL: data readback mismatch\n");
|
||||
return;
|
||||
}
|
||||
/* Test partial unmap in the middle */
|
||||
r = win_mem_release(test_addr + 16384, test_addr + 32768, NULL);
|
||||
if (r != 0) {
|
||||
printf("[SELFTEST] FAIL: middle release failed\n");
|
||||
return;
|
||||
}
|
||||
if (data[0] != 0x12345678 || data[test_size / 4 - 1] != 0x87654321) {
|
||||
printf("[SELFTEST] FAIL: remnant data corrupted after partial release\n");
|
||||
return;
|
||||
}
|
||||
win_mem_release(test_addr, test_addr + test_size, NULL);
|
||||
printf("[SELFTEST] PASS: win32_memory tests passed successfully.\n");
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
/* Windows guest address space (runtime_memory.c):
|
||||
* Provides memfd + mmap(MAP_FIXED) semantics on Windows.
|
||||
* Protection bits are PROT_READ 1, PROT_WRITE 2, PROT_EXEC 4. */
|
||||
#ifndef BB_WIN32_MEMORY_H
|
||||
#define BB_WIN32_MEMORY_H
|
||||
#ifdef _WIN32
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Reserves [start,end) once as a placeholder: nothing else in the process can take it. */
|
||||
int win_mem_space(uintptr_t start, uintptr_t end);
|
||||
/* Pagefile-backed section of size bytes (the physical pool) and a host view of all of it. */
|
||||
int win_mem_section(uint64_t size, void **backing);
|
||||
/* Called for each piece of a view that had to be remapped around a removed range. */
|
||||
typedef void (*WinMemRestore)(uintptr_t start, uintptr_t end);
|
||||
/* Maps section offset phys at [a,b), replacing whatever was there. */
|
||||
int win_mem_map(uintptr_t a, uintptr_t b, uint64_t phys, int prot, WinMemRestore restore);
|
||||
/* Removes mappings in [a,b); the range stays reserved (no access). */
|
||||
int win_mem_release(uintptr_t a, uintptr_t b, WinMemRestore restore);
|
||||
int win_mem_protect(uintptr_t a, uintptr_t b, int prot);
|
||||
/* Committed private read/write memory at exactly [a,b) (host-owned guest-visible memory). */
|
||||
void *win_mem_private(uintptr_t a, uintptr_t b);
|
||||
/* Lazy discard/zero-on-reuse for direct memory release without blocking memset. */
|
||||
int win_mem_punch(uint64_t phys, uint64_t size);
|
||||
/* Opt-in memory selftest for verification. */
|
||||
void win_mem_run_selftest(void);
|
||||
#endif
|
||||
#endif
|
||||
@@ -5,3 +5,11 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT / 'scripts') not in sys.path:
|
||||
sys.path.insert(0, str(ROOT / 'scripts'))
|
||||
|
||||
for candidate in ['bb-probe.exe', 'bbport.exe', 'bb-probe', 'bbport']:
|
||||
p = ROOT / 'out' / candidate
|
||||
if p.exists():
|
||||
EXE = p
|
||||
break
|
||||
else:
|
||||
EXE = ROOT / 'out' / 'bbport.exe'
|
||||
+15
-2
@@ -9,16 +9,29 @@ uintptr_t runtime_lookup(const RuntimeExport *table,size_t n,const char *name) {
|
||||
return 0;
|
||||
}
|
||||
int main(void) {
|
||||
#ifdef _WIN32
|
||||
char temp_dir[MAX_PATH];
|
||||
GetTempPathA(sizeof(temp_dir), temp_dir);
|
||||
char root[512];
|
||||
snprintf(root, sizeof(root), "%sbbport-mod-%lu", temp_dir, (unsigned long)GetCurrentProcessId());
|
||||
mkdir(root, 0755);
|
||||
#else
|
||||
char root[]="/tmp/bbport-mod-files-XXXXXX";
|
||||
assert(mkdtemp(root));
|
||||
char game[512],user[512],source[512],link[512];
|
||||
#endif
|
||||
char game[1024],user[1024],source[1024],link[1024];
|
||||
snprintf(game,sizeof(game),"%s/game",root);
|
||||
snprintf(user,sizeof(user),"%s/user",root);
|
||||
snprintf(source,sizeof(source),"%s/mod.dcx",root);
|
||||
snprintf(link,sizeof(link),"%s/game/asset.dcx",root);
|
||||
assert(!mkdir(game,0755));
|
||||
FILE *f=fopen(source,"w"); assert(f); assert(fputs("modded",f)>=0); assert(!fclose(f));
|
||||
#ifdef _WIN32
|
||||
/* Windows hard link works on any NTFS volume without elevated symlink privileges */
|
||||
assert(CreateHardLinkA(link, source, NULL));
|
||||
#else
|
||||
assert(!symlink(source,link));
|
||||
#endif
|
||||
runtime_file_configure(game,user);
|
||||
for (int i=0;i<3;++i) {
|
||||
const char *path=i==0 ? "/app0/asset.dcx" : i==1 ? "/hostapp/asset.dcx" : "asset.dcx";
|
||||
@@ -47,7 +60,7 @@ int main(void) {
|
||||
assert(!path_op("/data/test-save",2,0));
|
||||
assert(!unlink(link) && !unlink(source) && !rmdir(game));
|
||||
const char *dirs[]={"temp0","download0","data"};
|
||||
for (int i=0;i<3;++i) { char p[1024]; snprintf(p,sizeof(p),"%s/%s",user,dirs[i]); assert(!rmdir(p)); }
|
||||
for (int i=0;i<3;++i) { char p[2048]; snprintf(p,sizeof(p),"%s/%s",user,dirs[i]); assert(!rmdir(p)); }
|
||||
assert(!rmdir(user) && !rmdir(root));
|
||||
puts("Guest mod files: reads, stat, merged listing, readonly assets and writable saves PASS");
|
||||
}
|
||||
@@ -6,7 +6,11 @@
|
||||
#include <setjmp.h>
|
||||
// Renderer tests have no guest process. Clock/host-thread services work; guest accesses abort.
|
||||
extern "C" {
|
||||
#ifdef _WIN32
|
||||
thread_local jmp_buf* runtime_fault_recover = nullptr;
|
||||
#else
|
||||
thread_local sigjmp_buf* runtime_fault_recover = nullptr;
|
||||
#endif
|
||||
uint32_t runtime_disabled_optimizations = 0;
|
||||
uint64_t runtime_tsc_frequency() { return 1000000000; }
|
||||
int runtime_file_translate(const char*, char*, size_t) { std::abort(); }
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <assert.h>
|
||||
#include <unistd.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
static inline int setenv(const char *name, const char *value, int overwrite) {
|
||||
(void)overwrite;
|
||||
return _putenv_s(name, value);
|
||||
}
|
||||
#define usleep(us) Sleep((us) / 1000)
|
||||
#endif
|
||||
#include "../src/runtime_pad.c"
|
||||
|
||||
static int capture;
|
||||
@@ -19,7 +29,14 @@ static void inject(const char *path, const char *tokens) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
#ifdef _WIN32
|
||||
char temp_dir[MAX_PATH];
|
||||
GetTempPathA(sizeof(temp_dir), temp_dir);
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%sbbport-pad-test-XXXXXX", temp_dir);
|
||||
#else
|
||||
char path[]="/tmp/bbport-pad-test-XXXXXX";
|
||||
#endif
|
||||
int fd=mkstemp(path);
|
||||
assert(fd>=0);
|
||||
close(fd);
|
||||
|
||||
+1
-3
@@ -1,13 +1,11 @@
|
||||
"""Boundary tests for the native loader; uses tiny synthetic x86-64 images."""
|
||||
from paths import ROOT
|
||||
from paths import ROOT, EXE
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
EXE = ROOT / 'out/bb-probe'
|
||||
|
||||
|
||||
def package(code, relocs=(), names=(), capabilities=None):
|
||||
image = code.ljust(4096, b'\0')
|
||||
|
||||
@@ -5,14 +5,33 @@
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
#include <unistd.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
static inline int setenv(const char *name, const char *value, int overwrite) {
|
||||
(void)overwrite;
|
||||
return _putenv_s(name, value);
|
||||
}
|
||||
static inline int unsetenv(const char *name) {
|
||||
return _putenv_s(name, "");
|
||||
}
|
||||
#endif
|
||||
#include "bbport_settings.h"
|
||||
|
||||
int main() {
|
||||
using namespace BbSettings;
|
||||
#ifdef _WIN32
|
||||
char temp_dir[MAX_PATH];
|
||||
GetTempPathA(sizeof(temp_dir), temp_dir);
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%sbbport-upscaler-test-%lu", temp_dir, (unsigned long)GetCurrentProcessId());
|
||||
FILE *init_f = fopen(path, "w");
|
||||
if (init_f) fclose(init_f);
|
||||
#else
|
||||
char path[] = "/tmp/bbport-upscaler-test-XXXXXX";
|
||||
const int fd = mkstemp(path);
|
||||
assert(fd >= 0);
|
||||
close(fd);
|
||||
#endif
|
||||
setenv("BB_CONFIG", path, 1);
|
||||
unsetenv("BB_UPSCALER");
|
||||
unsetenv("BB_UPSCALE_PRESET");
|
||||
|
||||
Reference in new issue
Block a user