High Roughness, Sharp Highlights: Auditing a glTF Material in Three.js
An imported car-body model still showed sharp highlights on its hood and rear fender after switching to a preset labeled Matte. The overall reflection became weaker, but some bright patches remained.
That observation prompted a material audit: what did the preset actually change, and what properties had arrived with the imported asset?
Affiliation: This case comes from WrapPrism, a project I work on.
The browser capture session and file checks described below took place on October 27, 2026.
Establishing a comparable capture sequence
The comparison used one BMW M4 G82 ADRO-kit body study in the same Chrome tab and the same initial front three-quarter view. Between captures, there was no rotation, zoom, camera reset, vehicle switch, navigation, or window resizing.
| Recorded item | Capture conditions |
| --- | --- |
| Renderer marker | Three.js r186 |
| Viewport | 1470 × 775 CSS pixels; device pixel ratio 2 |
| 3D canvas | 881 × 565 CSS pixels; drawing buffer 1762 × 1130 |
| Reported camera distance | 7.8000 in every capture |
| Environment | Same RoomEnvironment / PMREM setup |
| Lighting configuration | Hemisphere light intensity 1.2; directional light intensity 2.5 at (4, 8, 3) |
| Exposure | 0.9 |
| Base-color inputs | #AB645B and #343B3A |
| Finish presets | Gloss, Satin, Matte |
The lighting settings were checked against the inspected application source snapshot and the script loaded by the browser. GPU uniforms were not inspected directly. Camera azimuth, elevation, and the complete Chrome version were not recorded either, so this is an application-level comparison with repeatability evidence, rather than a claim of pixel-identical reproduction across devices.
Two colors across three finishes produced six conditions. Two return-to-baseline captures brought the total to eight unmodified screenshots.
Color changes were visible on the hood, doors, and fenders, while the glass, wheels, and dark carbon components retained their appearance. Both colors showed changes between Gloss and Matte, although some sharp highlights remained under Matte.
The dark-gray sequence was Satin → Gloss → Matte → Satin. A second sequence switched away from the copper-colored Satin condition and then restored it. In both cases, the returning screenshot had the same whole-file SHA-256 hash as its corresponding baseline. That supports the absence of visible framing or scene drift within these sequences.
The original screenshots and model binary are not distributed with this article because their source-license provenance still needs clarification. The tables below summarize retained internal records; the exact asset-specific images cannot be independently reproduced from this article alone.
One preset changed more than one parameter
The finish mapping was verified in the script loaded by the browser:
| Preset | Base roughness | Metalness |
| --- | ---: | ---: |
| Gloss | 0.16 | 0.24 |
| Satin | 0.42 | 0.24 |
| Matte | 0.86 | 0.02 |
Of these two parameters, only roughness changed between Gloss and Satin. Switching to Matte also changed metalness.
Consequently, the full three-preset comparison does not isolate roughness. Three.js defines roughness and metalness as separate material properties; metalness is not a general-purpose gloss control. MeshStandardMaterial documentation
The original rationale for these values was not established, and no physical film samples were used for calibration. They are application presets, not universal physical definitions of gloss, satin, or matte.
To check that the changes extended beyond the selected UI control, the screenshots were compared using mean absolute differences in decoded RGB values:
| Comparison | Mean absolute difference, 0–255 |
| --- | ---: |
| Copper Satin / dark-gray Satin | 6.996 |
| Dark-gray Satin / dark-gray Gloss | 1.047 |
| Dark-gray Gloss / dark-gray Matte | 1.765 |
| Copper Satin / copper Gloss | 0.700 |
| Copper Gloss / copper Matte | 1.542 |
| Each of the two baseline-return comparisons | 0 |
The comparison region was the same screenshot rectangle: x = 100…959, y = 130…684. It includes background pixels, and the originals are JPEGs. These values establish image changes and returns to baseline within this capture session. They are not ΔE measurements, physical gloss readings, or material-accuracy scores.
The imported material had another reflective layer
The paint material in the actual GLB contained these extension fields:
{
"extensions": {
"KHR_materials_clearcoat": {
"clearcoatFactor": 1,
"clearcoatRoughnessFactor": 0.04
}
}
}
The Khronos extension defines a clearcoat layer over the base material, with its own intensity and roughness. Setting clearcoatFactor to zero disables that layer. KHR_materials_clearcoat specification
Three.js exposes the corresponding clearcoat and clearcoatRoughness properties. A rough base can therefore have a much smoother coating above it. MeshPhysicalMaterial documentation
The r186 loader provides a concrete explanation of how those properties enter the scene. When the clearcoat extension is present, GLTFLoader selects MeshPhysicalMaterial and maps the glTF factors to its clearcoat properties. Three.js r186 GLTFLoader source
The inspected application path then clones the imported material and replaces its base roughness, metalness, and color map without resetting clearcoat. The r186 material-copy implementation preserves the clearcoat properties and associated maps. Three.js r186 material-copy source
Along that code path, a base roughness of 0.86 can coexist with a clearcoat roughness of 0.04. The base roughness value alone does not describe the complete material.
This is a plausible explanation for the remaining highlights, not an experimentally established cause. The capture session did not include a clearcoat-disabled comparison or direct inspection of the final material uniforms. Surface normals, bright regions in the environment, and other material settings can also affect the image. A preset named Matte does not imply zero reflection.
A follow-up comparison that could distinguish the causes
The next diagnostic step would keep the model, camera, color, base roughness, metalness, and environment fixed in an isolated scene:
| Proposed condition | Base roughness | Metalness | Clearcoat | Clearcoat roughness |
| --- | ---: | ---: | ---: | ---: |
| A: retain the coating | 0.86 | 0.02 | 1 | 0.04 |
| B: disable only the coating | 0.86 | 0.02 | 0 | 0.04 |
| C: increase only coating roughness | 0.86 | 0.02 | 1 | 0.86 |
A/B would examine the coating's contribution. A/C would examine the effect of its roughness. These comparisons have not been performed, and no results are claimed here. The values are diagnostic choices, not calibrated settings for a real wrap film.
For a similar investigation:
1. Record the actual material type, asset version, and glTF extensions.
2. List every property changed by the UI action.
3. Inspect the base and coating layers separately, including their maps and normals.
4. Keep the camera, environment, exposure, and output pipeline fixed while changing one diagnostic variable.
5. Return to the starting condition and capture it again.
Keep the color pipeline consistent as well. Three.js distinguishes color inputs, its linear working space, and display output. A base-color input is not a promise that the illuminated body will have the same pixel values. Color textures and roughness-data textures also require different color-space treatment. Three.js Color Management manual
The eight captures establish recoverable image changes between color and finish presets. The asset fields, loader, and copy implementation establish a documented route by which an imported coating can survive those preset changes. Whether that coating caused the particular sharp highlights remains a question for the next controlled comparison.
5 reactions · 1 comment
Inside AnyPS5: static ELF relinking, NID symbol resolution, the 5-stage RDNA 2 shader recompiler, and what '100% shader coverage' means
In early October 2026, reports in Tom's Hardware, VideoCardz, TechPowerUp, and GIGAZINE covered a milestone in the open-source boykopovar/AnyPS5: the GPU shader instruction counter reached 100.0% (1,166 / 1,166 instructions), while system-library implementation rose from 84.76% (2,575 / 3,038) on October 9 to 88.3% (3,239 / 3,668) in the October 14, 2026 upstream snapshot.
When those numbers appeared alongside desktop clips of commercial titles (Animal Well, PAC-MAN WORLD Re-PAC, Dead Cells) and first-party bring-up tests (Demon's Souls, Astro Bot, Gran Turismo 7, Ghost of Yōtei), two misconceptions spread quickly in forum threads. First, many readers assumed the project boots PlayStation 5 firmware inside a virtual machine or JIT-translates CPU instructions frame by frame like a seventh-generation console emulator. Second, seeing a 1,166 / 1,166 shader badge and an 88.3% library counter led players to ask why Demon's Souls still freezes on Continue Offline or why Ghost of Yōtei stalls on its splash screen.
The upstream C++23 source tree answers both questions directly. Inspecting the ARCHITECTURE.md specification, the static relinker pipeline under core/relinker/, the replacement .prx runtime in core/libs/prx/, the 5-stage shader compiler in core/gpu/Shaders/Recompiler.cpp, and the AST/token scanner documented in docs/dev/PROGRESS.md shows how AnyPS5 converts a clean PS5 ELF into a native Windows or Linux executable, how its runtime maps FreeBSD-derived Sony kernel semantics onto Win32 and POSIX primitives, and why per-commit behavior is tracked separately in the community AnyPS5 guide.
1. Static binary relinking vs. virtual machine emulation
Console emulators for architectures like PowerPC or the Cell Broadband Engine (such as RPCS3) translate a foreign CPU instruction set into x86-64 or ARM64 machine code through an ahead-of-time (AOT) or just-in-time (JIT) recompiler while emulating privileged supervisor states.
Because the PlayStation 5's AMD Zen 2 CPU is already an x86-64 processor executing unprivileged user-space instructions for game code, AnyPS5 skips CPU virtualization. Similar to Wine and Proton on Linux or user-mode translation layers like spine and shadPS4, it divides execution into offline static relinking and runtime library replacement.
!AnyPS5 Static Relinker and NID Symbol Resolution Pipeline
Figure 1: The two-phase AnyPS5 execution model: offline static binary relinking (core/relinker/) paired with build-time NID export patching (nid_patcher) and host OS loader resolution.
Inside core/relinker: from Orbis/Prospero ELF to host PE/ELF
When you run the relinker on a clean, decrypted PS5 executable (eboot.bin or input.elf; encrypted packages and SELF/fSELF containers are rejected during header validation), the CLI entrypoint (core/relinker/main.cpp) passes the input tree to RelinkerPipeline (core/relinker/RelinkerPipeline.cpp).
RelinkerPipeline parses the PlayStation ELF segments, dynamic tables, and module import descriptors, and emits a native host executable without wrapping the guest binary in a custom loader stub:
- For the default Linux x86-64 target, the relinker rebuilds the ELF program headers, dynamic symbol tables (.dynsym / .dynstr), and relocation tables so the standard Linux dynamic linker (ld-linux-x86-64.so.2) can map and execute the binary directly.
- For the Windows x86-64 target (--windows), it reconstructs the binary as a Portable Executable (.exe) with a standard PE32+ header, Import Address Table (IAT), and section layout compatible with the Windows NT loader (ntdll.dll). It pins the image base to a 64 KiB-aligned non-ASLR address (--windows-fixed-base <address>, constrained between 0x20000 and 0x7fffffff0000).
- For companion guest modules, GuestModuleBuilder (core/relinker/GuestModuleBuilder.cpp) converts the engine plugins, audio middleware, and physics libraries bundled inside sce_module/, sce_modules/, or prx/ into matching host shared libraries while preserving their relative runtime paths under app0/.
Static instruction rewriting: --to-intel, --to-rosetta, and dead-export compaction
Although both the PS5's Zen 2 CPU and a desktop PC use x86-64, instruction set extensions and microarchitectural behaviors differ across AMD, Intel, and Apple Silicon (Rosetta 2) hosts. During static analysis, the relinker can decode and rewrite executable code sections before emitting the final binary:
| Relinker flag | Target host scenario | Static binary transformation performed | Fundamental boundary |
| :--- | :--- | :--- | :--- |
| (Default) | AMD Zen 2+ (Windows / Linux) | Preserves native user-mode x86-64 instruction stream; patches dynamic imports, relocations, and TLS/ABI thunks. | Requires host CPU support for instructions emitted by the PS5 toolchain. |
| --to-intel | Intel x86-64 hosts | Statically scans code sections in the main ELF and bundled modules, rewriting supported AMD-specific instructions into Intel-compatible equivalents. | Cannot rewrite dynamically generated code (JIT shaders, runtime unpackers, or self-modifying code). |
| --to-rosetta | macOS Apple Silicon (Rosetta 2 testing) | Rewrites x86-64 instructions unsupported by Apple's Rosetta 2 translator: lowers RDSEED to RDRAND, turns CLWB (Cache Line Write Back) into NOP, and replaces RDPID with a zeroed destination register. | Experimental translator aid; does not bypass missing macOS Vulkan/kernel primitives. |
| <code>--unused-filter=0|1|2</code> | Import / export graph compaction | 0 disables pruning; 1 runs conservative control-flow pruning of unreachable module paths; 2 (default) adds GOT/PLT reference checks to strip unused imported symbols. | Aggressive pruning (2) relies on static reachability analysis of GOT/PLT slots. |
--to-intel operates strictly at relink time. If a game's scripting VM or anti-tamper layer generates AMD-specific opcodes into executable heap pages at runtime, a static relinker never sees those bytes.
2. Clean-room .prx system libraries and the NID linking mechanism
Once the relinker emits app.exe (or app.elf), the binary still needs to call PlayStation 5 operating system services without Sony firmware.
On a physical PS5, executables rarely import system functions by human-readable C names like sceKernelMapDirectMemory. Instead, the PlayStation toolchain links against NIDs (Name Identifiers), which are 11-character Base64-like cryptographic hashes derived from the symbol name and a Sony ABI obfuscation suffix stored in the import table.
How nid_patcher turns standard C++ into NID exports
The core/libs/prx/ directory in the upstream repository contains more than 80 module subdirectories (libkernel, libSceAgc, libSceAgcDriver, libSceAudioOut, libScePad, libSceSaveData, libSceSysmodule, libSceVideoOut, and others). Each directory compiles into a standard host dynamic library named <module>.prx inside the runtime libs/ folder.
Writing C++ functions named after raw 11-character NID hashes would make the codebase unreadable and fail to compile whenever a hash contains characters outside [A-Za-z0-9_] (such as + or -). AnyPS5 handles this with a two-step build process documented in ARCHITECTURE.md:
1. Developers write standard C++ functions using descriptive symbol names and annotate them with APS5_VABI (enforcing the System V AMD64 ABI used by PS5 guest code, even on Windows where the host ABI is Microsoft x64) and APS5_EXPORT("<nid>", func).
2. After CMake links each .prx dynamic library (cmake/PrxLibrary.cmake), the build runs nid_patcher (tools/nid_patcher/main.cpp). nid_patcher parses the compiled ELF .dynsym/.dynstr or PE Export Address Table (EAT) and overwrites the exported symbol string in place with the 11-character NID.
Because both the relinked game binary (app.exe / app.elf) and the host .prx libraries now share identical NID symbol strings, the operating system's native loader (ntdll.dll on Windows, ld.so on Linux) binds thousands of PS5 system calls at process startup without runtime hash-lookup overhead.
!Windows Deferred Guest Initialization and APR/AMM Dual-Queue Execution
Figure 2: Inside libkernel.prx: avoiding Windows DllMain loader-lock deadlocks via __aps5_guest_initialize (top) and servicing asynchronous file/memory batches via dual APR/AMM worker queues (bottom).
Inside libkernel.prx: bridging three OS mismatches
Replacing high-level libraries like libScePad (mapped to SDL3 gamepads and anyps5-input.ini profiles) is relatively direct. The harder systems work sits inside core/libs/prx/libkernel/, where FreeBSD-derived Orbis/Prospero kernel semantics meet Windows NT and Linux kernel rules.
A. Escaping the Windows DllMain Loader Lock (DynamicLoader.cpp)
On a PS5, dynamically loaded guest .prx modules execute their static constructors (_init / .init_array) with full freedom to spawn threads, allocate direct memory, or load additional modules. On Windows, running guest initialization inside DllMain(DLL_PROCESS_ATTACH) while the NT loader holds the global Loader Lock causes immediate deadlocks if guest code spawns a thread or calls LoadLibrary.
As implemented in core/libs/prx/libkernel/DynamicLoader.cpp and documented in ARCHITECTURE.md, the relinker emits a metadata table named __aps5_guest_initialize into converted Windows modules instead of wiring guest constructors into DllMain. The header of this table holds an atomic 32-bit state word:
- 0: Uninitialized.
- UINT32_MAX (0xFFFFFFFF): Initialization completed.
- Any other non-zero value: The Win32 Thread ID (GetCurrentThreadId()) of the thread currently initializing the module.
After LoadLibrary returns and the Windows Loader Lock is released, libkernel walks the module's dependency graph depth-first, uses compare-and-swap on the state word to claim ownership (allowing recursive re-entry by the same Thread ID while blocking concurrent threads until UINT32_MAX is written), and executes the module's initialization pointers in normal user context.
B. FreeBSD libthr-modeled pthread cancellation (Thread.cpp & Cancel.hpp)
PlayStation 5 system threads use POSIX thread cancellation (scePthreadCancel, deferred vs. asynchronous modes, and cleanup handler stacks). On Windows, and across different libc implementations, native thread termination does not match Sony's cancellation-point semantics.
To match those rules across platforms, core/libs/prx/libkernel/Thread.cpp and Cancel.hpp port the cancellation state-machine design from FreeBSD's libthr (thr_cancel.c and thr_exit.c). Blocking libkernel calls wrap waits in CancelPointScope, checking per-thread cancellation flags before and after kernel waits and unwinding registered guest cleanup handlers in LIFO order before exiting the thread.
C. Asynchronous I/O and memory command buffers (Apr.cpp)
PS5 games stream multi-gigabyte assets using APR (sceKernelAprSubmitCommandBuffer*) and AMM (sceKernelAmmSubmitCommandBuffer*). Instead of issuing individual synchronous read() or mmap() syscalls, the game builds a command packet stream in memory that combines file reads, priority changes, memory mappings, and completion event flags, then submits the batch.
In core/libs/prx/libkernel/Apr.cpp, AnyPS5 uses a dual-queue worker design:
1. A submission parser (ParseCommands / ParseAmmCommands) validates the guest command buffer and assigns a monotonically increasing tracking ID (id = m_nextId++).
2. File I/O batches go to the APR Worker Thread, while virtual and direct memory mapping batches go to the AMM Worker Thread, allowing disk reads and page-table updates to run concurrently.
3. Synchronous variants (*AndGetResult) block only on the specific submission's condition variable (WaitCompletion), while asynchronous batches signal guest event flags or completion buffers (WriteResult) in the background.
3. The 5-stage RDNA 1/2 to SPIR-V shader recompiler
The PlayStation 5 GPU is based on an AMD RDNA 2 architecture programmed through Sony's low-level AGC (libSceAgc / libSceAgcDriver) graphics API. Game engines pre-compile shaders into proprietary AMD GCN/RDNA machine code binaries and submit draw and compute work via Draw Command Buffers (DCB) and Asynchronous Compute Buffers (ACB) encoded with PM4 packets.
Desktop PCs cannot execute raw PS5 AGC command buffers or console-specific RDNA machine code directly. The host runtime in core/gpu/ translates both the command stream (core/gpu/Execution/Pm4.cpp) and the shader binaries (core/gpu/Shaders/Recompiler.cpp) into Vulkan 1.1 and Khronos SPIR-V.
!AnyPS5 5-Stage RDNA 1/2 to SPIR-V Shader Recompiler Pipeline
Figure 3: The end-to-end graphics pipeline in core/gpu/, from AGC PM4 packet decoding to the 5-stage shader recompiler (Recompiler.cpp) and Vulkan 1.1 pipeline creation.
Walking through core/gpu/Shaders/Recompiler.cpp
When the PM4 command processor encounters a draw or dispatch packet (DrawIndexAuto, DrawIndex2, DispatchDirect), it hashes the active shader binary and execution state (stage, user SGPR layout, vertex/export attributes). On a cache miss in the Variant Cache, Recompiler.cpp runs five stages:
1. Stage 1 (RdnaDecoder, core/gpu/Shaders/RdnaDecoder.cpp):
Parses the binary instruction stream across all seven RDNA 1 and RDNA 2 encoding families defined in the AMD RDNA 2 Instruction Set Architecture Reference Guide:
- SOP (SOP1, SOP2, SOPC, SOPK, SOPP): Scalar ALU operations, wave branches, and barrier/waitcnt control (s_waitcnt, s_barrier).
- SMEM: Scalar memory loads for constant buffers, descriptor tables, and root user data.
- VOP (VOP1, VOP2, VOP3, VOP3P, VOPC): Vector ALU floating-point and integer math, packed FP16 dot products, and wave-wide condition mask (VCC / EXEC) updates.
- MIMG: Multi-dimensional image sampling, mipmapped fetches, and typed texture reads and writes.
- DS: Local Data Share (LDS) workgroup shared memory atomics and swizzles.
- FLAT / GLOBAL / SCRATCH: Unified 64-bit Buffer Device Address (BDA) pointer loads, stores, and atomics.
2. Stage 2 (ControlFlow, core/gpu/Shaders/ControlFlow.cpp):
RDNA hardware manages divergent branches by manipulating the 32-lane or 64-lane EXEC (Execute Mask) register (s_and_saveexec_b64, s_or_b64 exec, s_cbranch_execz). SPIR-V requires structured control flow (OpSelectionMerge, OpLoopMerge) under the Khronos SPIR-V Specification. ControlFlow.cpp reconstructs a control-flow graph (CFG) of basic blocks, recovers structured loops and conditional branches from EXEC mask manipulations, and synthesizes divergence merge blocks.
3. Stage 3 (Translation, core/gpu/Shaders/Translation.cpp):
Lowers decoded RDNA instructions within the structured CFG into an intermediate representation. Scalar (s0..s105) and vector (v0..v255) register accesses map to SSA values, and console descriptor table pointers fetched via SMEM bridge to Vulkan descriptor bindings and VK_KHR_buffer_device_address 64-bit pointers.
4. Stage 4 (Optimization, core/gpu/Shaders/Optimization.cpp):
Runs dead-code elimination, constant folding, and flag coalescing so redundant scalar status bit calculations (SCC) and unused EXEC mask saves do not bloat the generated SPIR-V module.
5. Stage 5 (SpirvBackend, core/gpu/Shaders/SpirvBackend.cpp):
Emits the binary SPIR-V word stream and passes it to Khronos SPIRV-Tools (spvtools::Optimizer) before calling vkCreateShaderModule.
> Build-time patch for Khronos SPIRV-Tools:
> In cmake/PatchSpirvTools.cmake and docs/dev/TechnicalDebt.md, the build applies a local patch to the SPIRV-Tools submodule referencing KhronosGroup/SPIRV-Tools PR #6962. Unpatched upstream SPIRV-Tools showed high memory growth when optimizing large machine-translated shaders with dense control-flow graphs during runtime uber-shader compilation.
4. Deconstructing the upstream progress metrics: why 100% shaders ≠ 100% compatibility
The Python scanner in tools/progress.py generates the repository's headline counters. Understanding its rules explains two patterns that often confuse readers following AnyPS5 progress.
!AnyPS5 Upstream Progress Metrics Trajectory and Scanner Rules
Figure 4: Historical trajectory of AnyPS5's upstream progress counters (Sept 28 to Oct 14, 2026) and the AST/token counting rules in tools/progress.py.
Rule 1: how tools/progress.py counts system libraries (3,239 / 3,668, 88.3%)
According to docs/dev/PROGRESS.md, tools/progress.py does not compare the codebase against a master list of every undocumented function in Sony's PS5 firmware. Instead, it scans the C and C++ source files listed in core/libs/prx/*/CMakeLists.txt:
1. It strips comments and preprocessor directives, then locates every function definition annotated with the APS5_VABI calling-convention macro.
2. It tokenizes the function body ({ ... }). If the body contains the stub marker NotImplemented_nid_no_patch (or matches a recognized placeholder stub pattern), the function is counted as unimplemented. Any other non-stub APS5_VABI function body is counted as implemented.
3. If two .prx targets share a common .cpp file (shared_sources), the script deduplicates those definitions so they are counted once in the global total.
Why the percentage dipped on October 8 while implemented functions increased
The commit timeline in Figure 4 (tracked on the AnyPS5 progress dashboard) shows how denominator changes affect the ratio:
| Snapshot date (UTC) | Upstream commit | Implemented functions | Declared denominator | Reported library % | Shader decoder counter |
| :--- | :--- | :---: | :---: | :---: | :---: |
| 2026-09-28 | 43349d43 | 2,000 | 2,585 | 77.37% | 561 / 1,166 (48.11%) |
| 2026-10-05 | 75159a62 | 2,511 | 2,919 | 86.02% | 864 / 1,166 (74.10%) |
| 2026-10-08 | 1ff271fa | 2,560 (+49) | 3,026 (+107) | 84.60% (-1.42%) | 1,166 / 1,166 (100.0%) |
| 2026-10-09 | Press wave snapshot | 2,575 | 3,038 | 84.76% | 1,166 / 1,166 (100.0%) |
| 2026-10-14 | e2e65d91 | 3,239 (+664) | 3,668 (+630) | 88.30% | 1,166 / 1,166 (100.0%) |
Between October 5 (75159a62) and October 8 (1ff271fa), contributors implemented 49 new functions (2,511 → 2,560), yet the reported percentage dropped from 86.02% to 84.60%. During those three days, developers declared 107 newly discovered imports across .prx modules to test additional games, expanding the denominator from 2,919 to 3,026.
In addition, tools/progress.py marks a function as implemented as soon as it is no longer a NotImplemented_nid_no_patch stub. An early function that returns SCE_OK (0) or populates default struct fields carries the same weight in the numerator as the full implementation of sceKernelAprSubmitCommandBuffer.
Rule 2: why 1,166 / 1,166 shader coverage only measures Stage 1 (RdnaDecoder)
As Tom's Hardware's Zak Killian wrote when covering the milestone, reaching 100% shader instruction coverage is a major step forward, "but playing all PS5 games natively on PC is still far off."
In tools/progress.py, the 1,166 / 1,166 counter checks whether Stage 1 (RdnaDecoder.cpp) recognizes every instruction entry in the project's enumerated RDNA 1 and RDNA 2 instruction table across DS, FLAT, GLOBAL, MIMG, SMEM, SOP, and VOP.
Even when RdnaDecoder decodes 100% of a game's shader opcodes without throwing an unknown-instruction abort, a frame must still pass Stages 2 through 5 and the AGC command processor:
- Control-flow structuring edge cases (Stage 2): Complex wave-ballot loops or irreducible EXEC-mask state machines can still trip SPIR-V structured merge rules.
- Resource and descriptor semantics (Stage 3 and PM4): If an AGC PM4 packet binds a depth-stencil surface, HTILE metadata buffer, or DCC-compressed render target using a layout mode not yet handled by Pm4.cpp, the shader executes against zeroed or misaligned memory.
- Vulkan pipeline barriers and synchronization: On the PS5's unified memory architecture, shaders rely on hardware L2 cache coherency and s_waitcnt / s_barrier semantics. On a PC discrete GPU over PCIe, missing a vkCmdPipelineBarrier between an asynchronous compute pass and a graphics draw causes black screens, flickering geometry, or device-lost (VK_ERROR_DEVICE_LOST) crashes without changing the 1,166 / 1,166 shader counter.
5. Case studies: how architecture explains per-game reports
The test configurations cataloged in the AnyPS5 games database and interactive AnyPS5 game checker show which architectural layer each game exercises:
| Title & edition | Tested branch / commit | Observed runtime behavior | Architectural layer involved |
| :--- | :--- | :--- | :--- |
| Animal Well | Upstream main (61a2468, Oct 12, 2026) on Windows | Reaches title screen, menus, and active in-game gameplay with background music and ambient audio; no immediate blocker hit during session. | Custom 2D engine loop whose .prx imports, APR/file reads, and shader passes fit within implemented main paths. |
| PAC-MAN WORLD Re-PAC | Upstream main (ff92477, Oct 12, 2026) on Windows | Reaches menus, loads an existing save, and plays a level with audio when relinked with explicit --module-dir flags for Unity plugins; level-end transition remains under check. | Shows why GuestModuleBuilder needs extra --module-dir discovery paths for engine plugin folders outside sce_module/. |
| DOOM (PS5 homebrew by lowbit) | Upstream main (bf602ee & patched 4209e76) | Boots into gameplay on E1M1 with keyboard/mouse (bf602ee); with a two-file runtime fix on 4209e76, plays through E1M3 with music, SFX, and working save/load. | Shows how a small .prx runtime fix unlocks audio and save persistence without altering shader counters. |
| Demon's Souls (PPSA01341 / PPSA01342) | Upstream main (046b2cc, Linux) vs. 82-commit experimental branch (d406b42, Windows) | main reaches the initial title screen on Linux (PPSA01341); the 82-commit experimental branch on Windows (PPSA01342) advances into menus and intro movies before hanging on Continue Offline. | Even with 100% RdnaDecoder coverage, asynchronous streaming, save/network state transitions, and branch-specific fixes separate title-screen boot from menu navigation. |
| Astro Bot (PPSA21564) | Private integration tree (a2ca291, Windows) | Enters the Crash Site playable area with movement, camera control, and jumping when launched via a direct level argument (--level ...). | Bypasses early title and cutscene state machines via direct engine CLI arguments while exercising 3D physics and AGC rendering. |
| Gran Turismo 7 | Experimental branch (19736b1, Windows) | Reaches menus, garage view, and Music Rally / AutoDrive sequences, while visual corruption and instability remain open issues. | Illustrates the gap between shader opcode decoding (1,166 / 1,166) and full AGC render-target and barrier handling in a custom engine. |
| Dead Cells (PPSA15552, v01.000.000) | Dedicated game fork (OverkillLabs3/DC-PS5, Windows 11) | Playable sessions with music, SFX, and keyboard/gamepad controls; rumble, lightbar, and motion remain unimplemented. | Shows why community videos must be checked for repository provenance: game-specific forks carry custom runtime changes not merged into stock boykopovar/AnyPS5. |
Because behavior shifts across commits and branches, the AnyPS5 game checker records each report as a separate configuration card tied to its branch, commit hash, operating system, launch route, and eight verification checks (Launch, Menu, Gameplay, Finish, Audio, Inputs, Saves, Stability).
6. Building and testing AnyPS5 locally
To inspect the codebase or test a clean, decrypted ELF that you have the legal right to use:
1. Match your release artifacts: Pair relinker-v0.1.2 with the matching prx-windows-v0.1.2.zip or prx-linux-v0.1.2.tar.gz archive from the same tag, or build both the relinker and libs CMake targets from the same commit (cmake --build build --config Release --target relinker --parallel and --target libs).
2. Verify host hardware and OS limits first: Check your Vulkan 1.1 driver, host RAM plus pagefile commit margin (up to 13,824 MiB of direct memory on Windows), and /dev/udmabuf permissions on Linux against the AnyPS5 requirements reference.
3. Keep libs/ and app0/ separated: Place AnyPS5's host .prx libraries (plus libgcc_s_seh-1.dll, libstdc++-6.dll, and libwinpthread-1.dll on Windows) inside libs/, and place converted guest modules inside app0/ at the relative paths printed by Guest module: during relinking. Step-by-step commands are documented in the AnyPS5 setup and AnyPS5 GitHub guides.
References and primary sources
1. Upstream architecture specification (boykopovar/AnyPS5): docs/dev/ARCHITECTURE.md (commit a48c0066), covering the static relinker pipeline, nid_patcher symbol rewriting, __aps5_guest_initialize deferred Windows initialization, FreeBSD-modeled pthread cancellation, APR/AMM workers, and the 5-stage shader recompiler.
2. Upstream progress scanner rules (boykopovar/AnyPS5): docs/dev/PROGRESS.md (commit 43349d43), defining how tools/progress.py counts APS5_VABI definitions, NotImplemented_nid_no_patch stubs, shared_sources deduplication, and RdnaDecoder.cpp instruction recognition.
3. Upstream technical debt and SPIRV-Tools patch (boykopovar/AnyPS5): docs/dev/TechnicalDebt.md (commit a48c0066) and KhronosGroup/SPIRV-Tools Pull Request #6962.
4. Zak Killian, Tom's Hardware (Oct 10, 2026): AnyPS5 project reaches critical GPU milestone in race to enable running PS5 games natively on PC.
5. WhyCry, VideoCardz (Oct 9, 2026): AnyPS5 reaches 100% GPU shader instruction coverage for PS5-to-PC conversion.
6. TechPowerUp (Oct 2026): AnyPS5 Achieves Full PS5 GPU Shader Instruction Translation.
7. GIGAZINE (Oct 9, 2026): AnyPS5 is an open-source tool that allows you to play PS5 games on Windows and Linux.
8. AMD RDNA 2 Instruction Set Architecture Reference Guide: AMD GPUOpen / TechDocs (rdna2-shader-instruction-set-architecture.pdf) and AMD GPUOpen RDNA 2 Overview.
9. FreeBSD libthr pthread cancellation implementation: lib/libthr/thread/thr_cancel.c and lib/libthr/thread/thr_exit.c#L196-L259.
10. Khronos Vulkan and SPIR-V specifications: Khronos Vulkan Registry and Khronos SPIR-V Registry.
Mentions
Claude に「この GLB を商品写真にして」と頼んで、4 方向の写真を GPU なしで受け取るまで 手元に GPU も Blender も無い状態で、Claude に 3D モデルの商品写真を作ってもらえるかを試しました。使ったのは Khronos の公式見本 WaterBottle(CC0 の GLB)と、JANCTION Render の MCP サーバーです。 つなぎ方 Claude.ai なら「設定 → コネクタ → カスタムコネクタを追加」に render.janction.jp を入れます。最初の呼び出しで同意の画面が出て、鍵が作られます。登録やカードは要りません。Claude Code なら次の 1 行のあと、/mcp で認証します。 claude mcp add --transport http janction-render render.janction.jp 頼んだこと チャットに GLB のリンクを貼って、商品写真にしてほしいと頼みました。 Claude は render_preview を preset='product' で呼び、0・90・180・270 度の 4 枚を試し描きしました。返ってくるまで約 5 秒。返事には絵と一緒に批評(critic)が付いていて、結果は ok でした。この試し描きは無料の範囲で、請求はありません。 途中で見つかったこと 試し描きの後、サーバーが返す「次の一手」が、preset を付けない仕上げを勧めていました。そのまま仕上げると商品写真の設定(無地の背景・4 方向)が外れてしまいます。これはサービス側の不具合で、試し描きと同じ preset を引き継ぐよう直しました。いまは試し描きの後の案内も preset='product' のままです。 時間と料金 本番で同じファイルの仕上げを測った数字です。1440x1440 の商品写真 4 枚が GPU 13.9 秒で約 1.4 円、同じファイルの 24 コマ 720p の回転動画が GPU 85.2 秒で約 8.5 円でした。単価は 0.1 円/GPU 秒です。 新しい鍵には 100 円分の無料クレジット(カード不要)が付き、請求なしでカードを保存すると 400 円分が足されます(14 日間)。そのあとも試し描きは 1 日 120 GPU 秒まで無料です。チャージは 100 円から。1 回の依頼の支出の上限は既定で 3,000 円で、1 回で頼めるのは 3,000 コマまでです。最新の条件は 料金ページ にあります。 コマンド 1 行で試すなら エージェントを使わずに同じことをするなら、ターミナルで次の 1 行です。 uvx janction-render shots github.com/KhronosGroup/glTF-Sample-Assets/blob/m… 開示: この記事は JANCTION Render を運営しているジャスミーラボの者が書いています。
ktx-rs 4.4.2: a pure Rust port of Khronos KTX-Software (libktx + ktx CLI, Basis Universal included) Hi, I've released ktx-rs, an unofficial pure-Rust port of KTX-Software: libktx and the ktx CLI, with no C/C++ toolchain needed. Repo: qbart/ktx-rs AI disclosure: most of the translation was done with AI assistance (Claude). That’s why the port is checked against the 1:1 ported upstream tests and byte-for-byte comparisons with the C++ tools. I'm committed to maintaining it and keeping it in sync with upstream. Please try it on your textures and report any difference from upstream, any crash, or any API rough edge. Thanks!
High Roughness, Sharp Highlights: Auditing a glTF Material in Three.js An imported car-body model still showed sharp highlights on its hood and rear fender after switching to a preset labeled Matte. The overall reflection became weaker, but some bright patches remained. That observation prompted a material audit: what did the preset actually change, and what properties had arrived with the imported asset? Affiliation: This case comes from WrapPrism, a project I work on. The browser capture session and file checks described below took place on October 27, 2026. Establishing a comparable capture sequence The comparison used one BMW M4 G82 ADRO-kit body study in the same Chrome tab and the same initial front three-quarter view. Between captures, there was no rotation, zoom, camera reset, vehicle switch, navigation, or window resizing. | Recorded item | Capture conditions | | --- | --- | | Renderer marker | Three.js r186 | | Viewport | 1470 × 775 CSS pixels; device pixel ratio 2 | | 3D canvas | 881 × 565 CSS pixels; drawing buffer 1762 × 1130 | | Reported camera distance | 7.8000 in every capture | | Environment | Same RoomEnvironment / PMREM setup | | Lighting configuration | Hemisphere light intensity 1.2; directional light intensity 2.5 at (4, 8, 3) | | Exposure | 0.9 | | Base-color inputs | #AB645B and #343B3A | | Finish presets | Gloss, Satin, Matte | The lighting settings were checked against the inspected application source snapshot and the script loaded by the browser. GPU uniforms were not inspected directly. Camera azimuth, elevation, and the complete Chrome version were not recorded either, so this is an application-level comparison with repeatability evidence, rather than a claim of pixel-identical reproduction across devices. Two colors across three finishes produced six conditions. Two return-to-baseline captures brought the total to eight unmodified screenshots. Color changes were visible on the hood, doors, and fenders, while the glass, wheels, and dark carbon components retained their appearance. Both colors showed changes between Gloss and Matte, although some sharp highlights remained under Matte. The dark-gray sequence was Satin → Gloss → Matte → Satin. A second sequence switched away from the copper-colored Satin condition and then restored it. In both cases, the returning screenshot had the same whole-file SHA-256 hash as its corresponding baseline. That supports the absence of visible framing or scene drift within these sequences. The original screenshots and model binary are not distributed with this article because their source-license provenance still needs clarification. The tables below summarize retained internal records; the exact asset-specific images cannot be independently reproduced from this article alone. One preset changed more than one parameter The finish mapping was verified in the script loaded by the browser: | Preset | Base roughness | Metalness | | --- | ---: | ---: | | Gloss | 0.16 | 0.24 | | Satin | 0.42 | 0.24 | | Matte | 0.86 | 0.02 | Of these two parameters, only roughness changed between Gloss and Satin. Switching to Matte also changed metalness. Consequently, the full three-preset comparison does not isolate roughness. Three.js defines roughness and metalness as separate material properties; metalness is not a general-purpose gloss control. MeshStandardMaterial documentation The original rationale for these values was not established, and no physical film samples were used for calibration. They are application presets, not universal physical definitions of gloss, satin, or matte. To check that the changes extended beyond the selected UI control, the screenshots were compared using mean absolute differences in decoded RGB values: | Comparison | Mean absolute difference, 0–255 | | --- | ---: | | Copper Satin / dark-gray Satin | 6.996 | | Dark-gray Satin / dark-gray Gloss | 1.047 | | Dark-gray Gloss / dark-gray Matte | 1.765 | | Copper Satin / copper Gloss | 0.700 | | Copper Gloss / copper Matte | 1.542 | | Each of the two baseline-return comparisons | 0 | The comparison region was the same screenshot rectangle: x = 100…959, y = 130…684. It includes background pixels, and the originals are JPEGs. These values establish image changes and returns to baseline within this capture session. They are not ΔE measurements, physical gloss readings, or material-accuracy scores. The imported material had another reflective layer The paint material in the actual GLB contained these extension fields: { "extensions": { "KHR_materials_clearcoat": { "clearcoatFactor": 1, "clearcoatRoughnessFactor": 0.04 } } } The Khronos extension defines a clearcoat layer over the base material, with its own intensity and roughness. Setting clearcoatFactor to zero disables that layer. KHR_materials_clearcoat specification Three.js exposes the corresponding clearcoat and clearcoatRoughness properties. A rough base can therefore have a much smoother coating above it. MeshPhysicalMaterial documentation The r186 loader provides a concrete explanation of how those properties enter the scene. When the clearcoat extension is present, GLTFLoader selects MeshPhysicalMaterial and maps the glTF factors to its clearcoat properties. Three.js r186 GLTFLoader source The inspected application path then clones the imported material and replaces its base roughness, metalness, and color map without resetting clearcoat. The r186 material-copy implementation preserves the clearcoat properties and associated maps. Three.js r186 material-copy source Along that code path, a base roughness of 0.86 can coexist with a clearcoat roughness of 0.04. The base roughness value alone does not describe the complete material. This is a plausible explanation for the remaining highlights, not an experimentally established cause. The capture session did not include a clearcoat-disabled comparison or direct inspection of the final material uniforms. Surface normals, bright regions in the environment, and other material settings can also affect the image. A preset named Matte does not imply zero reflection. A follow-up comparison that could distinguish the causes The next diagnostic step would keep the model, camera, color, base roughness, metalness, and environment fixed in an isolated scene: | Proposed condition | Base roughness | Metalness | Clearcoat | Clearcoat roughness | | --- | ---: | ---: | ---: | ---: | | A: retain the coating | 0.86 | 0.02 | 1 | 0.04 | | B: disable only the coating | 0.86 | 0.02 | 0 | 0.04 | | C: increase only coating roughness | 0.86 | 0.02 | 1 | 0.86 | A/B would examine the coating's contribution. A/C would examine the effect of its roughness. These comparisons have not been performed, and no results are claimed here. The values are diagnostic choices, not calibrated settings for a real wrap film. For a similar investigation: 1. Record the actual material type, asset version, and glTF extensions. 2. List every property changed by the UI action. 3. Inspect the base and coating layers separately, including their maps and normals. 4. Keep the camera, environment, exposure, and output pipeline fixed while changing one diagnostic variable. 5. Return to the starting condition and capture it again. Keep the color pipeline consistent as well. Three.js distinguishes color inputs, its linear working space, and display output. A base-color input is not a promise that the illuminated body will have the same pixel values. Color textures and roughness-data textures also require different color-space treatment. Three.js Color Management manual The eight captures establish recoverable image changes between color and finish presets. The asset fields, loader, and copy implementation establish a documented route by which an imported coating can survive those preset changes. Whether that coating caused the particular sharp highlights remains a question for the next controlled comparison.
MoltenVK (Vulkan on Metal) was deprecated in favor of KosmicKrisp
nikki 和 momoten 两个驱动 id 已经正式合并到 vulkan-docs github.com/KhronosGroup/Vulkan-Docs/commit/67e309…
MoltenVKがコミュニティによるベストエフォートメンテナンスモードに入った。macOS on Apple SiliconでのVulkan on MetalはKosmicKrispを使用することが推奨される。 KhronosGroup/MoltenVK
There's in no path for that. Each GPU is different, and I suspect won't even let the CPU access the final framebuffer they use for video signals, even if you wrote the driver yourself. As for Windows compositing, use the graphics API to acquire exclusive fullscreen. Vulkan gives this example: github.com/KhronosGroup/Vulkan-Samples/tree/main/…
Inside AnyPS5: static ELF relinking, NID symbol resolution, the 5-stage RDNA 2 shader recompiler, and what '100% shader coverage' means In early October 2026, reports in Tom's Hardware, VideoCardz, TechPowerUp, and GIGAZINE covered a milestone in the open-source boykopovar/AnyPS5: the GPU shader instruction counter reached 100.0% (1,166 / 1,166 instructions), while system-library implementation rose from 84.76% (2,575 / 3,038) on October 9 to 88.3% (3,239 / 3,668) in the October 14, 2026 upstream snapshot. When those numbers appeared alongside desktop clips of commercial titles (Animal Well, PAC-MAN WORLD Re-PAC, Dead Cells) and first-party bring-up tests (Demon's Souls, Astro Bot, Gran Turismo 7, Ghost of Yōtei), two misconceptions spread quickly in forum threads. First, many readers assumed the project boots PlayStation 5 firmware inside a virtual machine or JIT-translates CPU instructions frame by frame like a seventh-generation console emulator. Second, seeing a 1,166 / 1,166 shader badge and an 88.3% library counter led players to ask why Demon's Souls still freezes on Continue Offline or why Ghost of Yōtei stalls on its splash screen. The upstream C++23 source tree answers both questions directly. Inspecting the ARCHITECTURE.md specification, the static relinker pipeline under core/relinker/, the replacement .prx runtime in core/libs/prx/, the 5-stage shader compiler in core/gpu/Shaders/Recompiler.cpp, and the AST/token scanner documented in docs/dev/PROGRESS.md shows how AnyPS5 converts a clean PS5 ELF into a native Windows or Linux executable, how its runtime maps FreeBSD-derived Sony kernel semantics onto Win32 and POSIX primitives, and why per-commit behavior is tracked separately in the community AnyPS5 guide. 1. Static binary relinking vs. virtual machine emulation Console emulators for architectures like PowerPC or the Cell Broadband Engine (such as RPCS3) translate a foreign CPU instruction set into x86-64 or ARM64 machine code through an ahead-of-time (AOT) or just-in-time (JIT) recompiler while emulating privileged supervisor states. Because the PlayStation 5's AMD Zen 2 CPU is already an x86-64 processor executing unprivileged user-space instructions for game code, AnyPS5 skips CPU virtualization. Similar to Wine and Proton on Linux or user-mode translation layers like spine and shadPS4, it divides execution into offline static relinking and runtime library replacement. !AnyPS5 Static Relinker and NID Symbol Resolution Pipeline Figure 1: The two-phase AnyPS5 execution model: offline static binary relinking (core/relinker/) paired with build-time NID export patching (nid_patcher) and host OS loader resolution. Inside core/relinker: from Orbis/Prospero ELF to host PE/ELF When you run the relinker on a clean, decrypted PS5 executable (eboot.bin or input.elf; encrypted packages and SELF/fSELF containers are rejected during header validation), the CLI entrypoint (core/relinker/main.cpp) passes the input tree to RelinkerPipeline (core/relinker/RelinkerPipeline.cpp). RelinkerPipeline parses the PlayStation ELF segments, dynamic tables, and module import descriptors, and emits a native host executable without wrapping the guest binary in a custom loader stub: - For the default Linux x86-64 target, the relinker rebuilds the ELF program headers, dynamic symbol tables (.dynsym / .dynstr), and relocation tables so the standard Linux dynamic linker (ld-linux-x86-64.so.2) can map and execute the binary directly. - For the Windows x86-64 target (--windows), it reconstructs the binary as a Portable Executable (.exe) with a standard PE32+ header, Import Address Table (IAT), and section layout compatible with the Windows NT loader (ntdll.dll). It pins the image base to a 64 KiB-aligned non-ASLR address (--windows-fixed-base <address>, constrained between 0x20000 and 0x7fffffff0000). - For companion guest modules, GuestModuleBuilder (core/relinker/GuestModuleBuilder.cpp) converts the engine plugins, audio middleware, and physics libraries bundled inside sce_module/, sce_modules/, or prx/ into matching host shared libraries while preserving their relative runtime paths under app0/. Static instruction rewriting: --to-intel, --to-rosetta, and dead-export compaction Although both the PS5's Zen 2 CPU and a desktop PC use x86-64, instruction set extensions and microarchitectural behaviors differ across AMD, Intel, and Apple Silicon (Rosetta 2) hosts. During static analysis, the relinker can decode and rewrite executable code sections before emitting the final binary: | Relinker flag | Target host scenario | Static binary transformation performed | Fundamental boundary | | :--- | :--- | :--- | :--- | | (Default) | AMD Zen 2+ (Windows / Linux) | Preserves native user-mode x86-64 instruction stream; patches dynamic imports, relocations, and TLS/ABI thunks. | Requires host CPU support for instructions emitted by the PS5 toolchain. | | --to-intel | Intel x86-64 hosts | Statically scans code sections in the main ELF and bundled modules, rewriting supported AMD-specific instructions into Intel-compatible equivalents. | Cannot rewrite dynamically generated code (JIT shaders, runtime unpackers, or self-modifying code). | | --to-rosetta | macOS Apple Silicon (Rosetta 2 testing) | Rewrites x86-64 instructions unsupported by Apple's Rosetta 2 translator: lowers RDSEED to RDRAND, turns CLWB (Cache Line Write Back) into NOP, and replaces RDPID with a zeroed destination register. | Experimental translator aid; does not bypass missing macOS Vulkan/kernel primitives. | | <code>--unused-filter=0|1|2</code> | Import / export graph compaction | 0 disables pruning; 1 runs conservative control-flow pruning of unreachable module paths; 2 (default) adds GOT/PLT reference checks to strip unused imported symbols. | Aggressive pruning (2) relies on static reachability analysis of GOT/PLT slots. | --to-intel operates strictly at relink time. If a game's scripting VM or anti-tamper layer generates AMD-specific opcodes into executable heap pages at runtime, a static relinker never sees those bytes. 2. Clean-room .prx system libraries and the NID linking mechanism Once the relinker emits app.exe (or app.elf), the binary still needs to call PlayStation 5 operating system services without Sony firmware. On a physical PS5, executables rarely import system functions by human-readable C names like sceKernelMapDirectMemory. Instead, the PlayStation toolchain links against NIDs (Name Identifiers), which are 11-character Base64-like cryptographic hashes derived from the symbol name and a Sony ABI obfuscation suffix stored in the import table. How nid_patcher turns standard C++ into NID exports The core/libs/prx/ directory in the upstream repository contains more than 80 module subdirectories (libkernel, libSceAgc, libSceAgcDriver, libSceAudioOut, libScePad, libSceSaveData, libSceSysmodule, libSceVideoOut, and others). Each directory compiles into a standard host dynamic library named <module>.prx inside the runtime libs/ folder. Writing C++ functions named after raw 11-character NID hashes would make the codebase unreadable and fail to compile whenever a hash contains characters outside [A-Za-z0-9_] (such as + or -). AnyPS5 handles this with a two-step build process documented in ARCHITECTURE.md: 1. Developers write standard C++ functions using descriptive symbol names and annotate them with APS5_VABI (enforcing the System V AMD64 ABI used by PS5 guest code, even on Windows where the host ABI is Microsoft x64) and APS5_EXPORT("<nid>", func). 2. After CMake links each .prx dynamic library (cmake/PrxLibrary.cmake), the build runs nid_patcher (tools/nid_patcher/main.cpp). nid_patcher parses the compiled ELF .dynsym/.dynstr or PE Export Address Table (EAT) and overwrites the exported symbol string in place with the 11-character NID. Because both the relinked game binary (app.exe / app.elf) and the host .prx libraries now share identical NID symbol strings, the operating system's native loader (ntdll.dll on Windows, ld.so on Linux) binds thousands of PS5 system calls at process startup without runtime hash-lookup overhead. !Windows Deferred Guest Initialization and APR/AMM Dual-Queue Execution Figure 2: Inside libkernel.prx: avoiding Windows DllMain loader-lock deadlocks via __aps5_guest_initialize (top) and servicing asynchronous file/memory batches via dual APR/AMM worker queues (bottom). Inside libkernel.prx: bridging three OS mismatches Replacing high-level libraries like libScePad (mapped to SDL3 gamepads and anyps5-input.ini profiles) is relatively direct. The harder systems work sits inside core/libs/prx/libkernel/, where FreeBSD-derived Orbis/Prospero kernel semantics meet Windows NT and Linux kernel rules. A. Escaping the Windows DllMain Loader Lock (DynamicLoader.cpp) On a PS5, dynamically loaded guest .prx modules execute their static constructors (_init / .init_array) with full freedom to spawn threads, allocate direct memory, or load additional modules. On Windows, running guest initialization inside DllMain(DLL_PROCESS_ATTACH) while the NT loader holds the global Loader Lock causes immediate deadlocks if guest code spawns a thread or calls LoadLibrary. As implemented in core/libs/prx/libkernel/DynamicLoader.cpp and documented in ARCHITECTURE.md, the relinker emits a metadata table named __aps5_guest_initialize into converted Windows modules instead of wiring guest constructors into DllMain. The header of this table holds an atomic 32-bit state word: - 0: Uninitialized. - UINT32_MAX (0xFFFFFFFF): Initialization completed. - Any other non-zero value: The Win32 Thread ID (GetCurrentThreadId()) of the thread currently initializing the module. After LoadLibrary returns and the Windows Loader Lock is released, libkernel walks the module's dependency graph depth-first, uses compare-and-swap on the state word to claim ownership (allowing recursive re-entry by the same Thread ID while blocking concurrent threads until UINT32_MAX is written), and executes the module's initialization pointers in normal user context. B. FreeBSD libthr-modeled pthread cancellation (Thread.cpp & Cancel.hpp) PlayStation 5 system threads use POSIX thread cancellation (scePthreadCancel, deferred vs. asynchronous modes, and cleanup handler stacks). On Windows, and across different libc implementations, native thread termination does not match Sony's cancellation-point semantics. To match those rules across platforms, core/libs/prx/libkernel/Thread.cpp and Cancel.hpp port the cancellation state-machine design from FreeBSD's libthr (thr_cancel.c and thr_exit.c). Blocking libkernel calls wrap waits in CancelPointScope, checking per-thread cancellation flags before and after kernel waits and unwinding registered guest cleanup handlers in LIFO order before exiting the thread. C. Asynchronous I/O and memory command buffers (Apr.cpp) PS5 games stream multi-gigabyte assets using APR (sceKernelAprSubmitCommandBuffer*) and AMM (sceKernelAmmSubmitCommandBuffer*). Instead of issuing individual synchronous read() or mmap() syscalls, the game builds a command packet stream in memory that combines file reads, priority changes, memory mappings, and completion event flags, then submits the batch. In core/libs/prx/libkernel/Apr.cpp, AnyPS5 uses a dual-queue worker design: 1. A submission parser (ParseCommands / ParseAmmCommands) validates the guest command buffer and assigns a monotonically increasing tracking ID (id = m_nextId++). 2. File I/O batches go to the APR Worker Thread, while virtual and direct memory mapping batches go to the AMM Worker Thread, allowing disk reads and page-table updates to run concurrently. 3. Synchronous variants (*AndGetResult) block only on the specific submission's condition variable (WaitCompletion), while asynchronous batches signal guest event flags or completion buffers (WriteResult) in the background. 3. The 5-stage RDNA 1/2 to SPIR-V shader recompiler The PlayStation 5 GPU is based on an AMD RDNA 2 architecture programmed through Sony's low-level AGC (libSceAgc / libSceAgcDriver) graphics API. Game engines pre-compile shaders into proprietary AMD GCN/RDNA machine code binaries and submit draw and compute work via Draw Command Buffers (DCB) and Asynchronous Compute Buffers (ACB) encoded with PM4 packets. Desktop PCs cannot execute raw PS5 AGC command buffers or console-specific RDNA machine code directly. The host runtime in core/gpu/ translates both the command stream (core/gpu/Execution/Pm4.cpp) and the shader binaries (core/gpu/Shaders/Recompiler.cpp) into Vulkan 1.1 and Khronos SPIR-V. !AnyPS5 5-Stage RDNA 1/2 to SPIR-V Shader Recompiler Pipeline Figure 3: The end-to-end graphics pipeline in core/gpu/, from AGC PM4 packet decoding to the 5-stage shader recompiler (Recompiler.cpp) and Vulkan 1.1 pipeline creation. Walking through core/gpu/Shaders/Recompiler.cpp When the PM4 command processor encounters a draw or dispatch packet (DrawIndexAuto, DrawIndex2, DispatchDirect), it hashes the active shader binary and execution state (stage, user SGPR layout, vertex/export attributes). On a cache miss in the Variant Cache, Recompiler.cpp runs five stages: 1. Stage 1 (RdnaDecoder, core/gpu/Shaders/RdnaDecoder.cpp): Parses the binary instruction stream across all seven RDNA 1 and RDNA 2 encoding families defined in the AMD RDNA 2 Instruction Set Architecture Reference Guide: - SOP (SOP1, SOP2, SOPC, SOPK, SOPP): Scalar ALU operations, wave branches, and barrier/waitcnt control (s_waitcnt, s_barrier). - SMEM: Scalar memory loads for constant buffers, descriptor tables, and root user data. - VOP (VOP1, VOP2, VOP3, VOP3P, VOPC): Vector ALU floating-point and integer math, packed FP16 dot products, and wave-wide condition mask (VCC / EXEC) updates. - MIMG: Multi-dimensional image sampling, mipmapped fetches, and typed texture reads and writes. - DS: Local Data Share (LDS) workgroup shared memory atomics and swizzles. - FLAT / GLOBAL / SCRATCH: Unified 64-bit Buffer Device Address (BDA) pointer loads, stores, and atomics. 2. Stage 2 (ControlFlow, core/gpu/Shaders/ControlFlow.cpp): RDNA hardware manages divergent branches by manipulating the 32-lane or 64-lane EXEC (Execute Mask) register (s_and_saveexec_b64, s_or_b64 exec, s_cbranch_execz). SPIR-V requires structured control flow (OpSelectionMerge, OpLoopMerge) under the Khronos SPIR-V Specification. ControlFlow.cpp reconstructs a control-flow graph (CFG) of basic blocks, recovers structured loops and conditional branches from EXEC mask manipulations, and synthesizes divergence merge blocks. 3. Stage 3 (Translation, core/gpu/Shaders/Translation.cpp): Lowers decoded RDNA instructions within the structured CFG into an intermediate representation. Scalar (s0..s105) and vector (v0..v255) register accesses map to SSA values, and console descriptor table pointers fetched via SMEM bridge to Vulkan descriptor bindings and VK_KHR_buffer_device_address 64-bit pointers. 4. Stage 4 (Optimization, core/gpu/Shaders/Optimization.cpp): Runs dead-code elimination, constant folding, and flag coalescing so redundant scalar status bit calculations (SCC) and unused EXEC mask saves do not bloat the generated SPIR-V module. 5. Stage 5 (SpirvBackend, core/gpu/Shaders/SpirvBackend.cpp): Emits the binary SPIR-V word stream and passes it to Khronos SPIRV-Tools (spvtools::Optimizer) before calling vkCreateShaderModule. > Build-time patch for Khronos SPIRV-Tools: > In cmake/PatchSpirvTools.cmake and docs/dev/TechnicalDebt.md, the build applies a local patch to the SPIRV-Tools submodule referencing KhronosGroup/SPIRV-Tools PR #6962. Unpatched upstream SPIRV-Tools showed high memory growth when optimizing large machine-translated shaders with dense control-flow graphs during runtime uber-shader compilation. 4. Deconstructing the upstream progress metrics: why 100% shaders ≠ 100% compatibility The Python scanner in tools/progress.py generates the repository's headline counters. Understanding its rules explains two patterns that often confuse readers following AnyPS5 progress. !AnyPS5 Upstream Progress Metrics Trajectory and Scanner Rules Figure 4: Historical trajectory of AnyPS5's upstream progress counters (Sept 28 to Oct 14, 2026) and the AST/token counting rules in tools/progress.py. Rule 1: how tools/progress.py counts system libraries (3,239 / 3,668, 88.3%) According to docs/dev/PROGRESS.md, tools/progress.py does not compare the codebase against a master list of every undocumented function in Sony's PS5 firmware. Instead, it scans the C and C++ source files listed in core/libs/prx/*/CMakeLists.txt: 1. It strips comments and preprocessor directives, then locates every function definition annotated with the APS5_VABI calling-convention macro. 2. It tokenizes the function body ({ ... }). If the body contains the stub marker NotImplemented_nid_no_patch (or matches a recognized placeholder stub pattern), the function is counted as unimplemented. Any other non-stub APS5_VABI function body is counted as implemented. 3. If two .prx targets share a common .cpp file (shared_sources), the script deduplicates those definitions so they are counted once in the global total. Why the percentage dipped on October 8 while implemented functions increased The commit timeline in Figure 4 (tracked on the AnyPS5 progress dashboard) shows how denominator changes affect the ratio: | Snapshot date (UTC) | Upstream commit | Implemented functions | Declared denominator | Reported library % | Shader decoder counter | | :--- | :--- | :---: | :---: | :---: | :---: | | 2026-09-28 | 43349d43 | 2,000 | 2,585 | 77.37% | 561 / 1,166 (48.11%) | | 2026-10-05 | 75159a62 | 2,511 | 2,919 | 86.02% | 864 / 1,166 (74.10%) | | 2026-10-08 | 1ff271fa | 2,560 (+49) | 3,026 (+107) | 84.60% (-1.42%) | 1,166 / 1,166 (100.0%) | | 2026-10-09 | Press wave snapshot | 2,575 | 3,038 | 84.76% | 1,166 / 1,166 (100.0%) | | 2026-10-14 | e2e65d91 | 3,239 (+664) | 3,668 (+630) | 88.30% | 1,166 / 1,166 (100.0%) | Between October 5 (75159a62) and October 8 (1ff271fa), contributors implemented 49 new functions (2,511 → 2,560), yet the reported percentage dropped from 86.02% to 84.60%. During those three days, developers declared 107 newly discovered imports across .prx modules to test additional games, expanding the denominator from 2,919 to 3,026. In addition, tools/progress.py marks a function as implemented as soon as it is no longer a NotImplemented_nid_no_patch stub. An early function that returns SCE_OK (0) or populates default struct fields carries the same weight in the numerator as the full implementation of sceKernelAprSubmitCommandBuffer. Rule 2: why 1,166 / 1,166 shader coverage only measures Stage 1 (RdnaDecoder) As Tom's Hardware's Zak Killian wrote when covering the milestone, reaching 100% shader instruction coverage is a major step forward, "but playing all PS5 games natively on PC is still far off." In tools/progress.py, the 1,166 / 1,166 counter checks whether Stage 1 (RdnaDecoder.cpp) recognizes every instruction entry in the project's enumerated RDNA 1 and RDNA 2 instruction table across DS, FLAT, GLOBAL, MIMG, SMEM, SOP, and VOP. Even when RdnaDecoder decodes 100% of a game's shader opcodes without throwing an unknown-instruction abort, a frame must still pass Stages 2 through 5 and the AGC command processor: - Control-flow structuring edge cases (Stage 2): Complex wave-ballot loops or irreducible EXEC-mask state machines can still trip SPIR-V structured merge rules. - Resource and descriptor semantics (Stage 3 and PM4): If an AGC PM4 packet binds a depth-stencil surface, HTILE metadata buffer, or DCC-compressed render target using a layout mode not yet handled by Pm4.cpp, the shader executes against zeroed or misaligned memory. - Vulkan pipeline barriers and synchronization: On the PS5's unified memory architecture, shaders rely on hardware L2 cache coherency and s_waitcnt / s_barrier semantics. On a PC discrete GPU over PCIe, missing a vkCmdPipelineBarrier between an asynchronous compute pass and a graphics draw causes black screens, flickering geometry, or device-lost (VK_ERROR_DEVICE_LOST) crashes without changing the 1,166 / 1,166 shader counter. 5. Case studies: how architecture explains per-game reports The test configurations cataloged in the AnyPS5 games database and interactive AnyPS5 game checker show which architectural layer each game exercises: | Title & edition | Tested branch / commit | Observed runtime behavior | Architectural layer involved | | :--- | :--- | :--- | :--- | | Animal Well | Upstream main (61a2468, Oct 12, 2026) on Windows | Reaches title screen, menus, and active in-game gameplay with background music and ambient audio; no immediate blocker hit during session. | Custom 2D engine loop whose .prx imports, APR/file reads, and shader passes fit within implemented main paths. | | PAC-MAN WORLD Re-PAC | Upstream main (ff92477, Oct 12, 2026) on Windows | Reaches menus, loads an existing save, and plays a level with audio when relinked with explicit --module-dir flags for Unity plugins; level-end transition remains under check. | Shows why GuestModuleBuilder needs extra --module-dir discovery paths for engine plugin folders outside sce_module/. | | DOOM (PS5 homebrew by lowbit) | Upstream main (bf602ee & patched 4209e76) | Boots into gameplay on E1M1 with keyboard/mouse (bf602ee); with a two-file runtime fix on 4209e76, plays through E1M3 with music, SFX, and working save/load. | Shows how a small .prx runtime fix unlocks audio and save persistence without altering shader counters. | | Demon's Souls (PPSA01341 / PPSA01342) | Upstream main (046b2cc, Linux) vs. 82-commit experimental branch (d406b42, Windows) | main reaches the initial title screen on Linux (PPSA01341); the 82-commit experimental branch on Windows (PPSA01342) advances into menus and intro movies before hanging on Continue Offline. | Even with 100% RdnaDecoder coverage, asynchronous streaming, save/network state transitions, and branch-specific fixes separate title-screen boot from menu navigation. | | Astro Bot (PPSA21564) | Private integration tree (a2ca291, Windows) | Enters the Crash Site playable area with movement, camera control, and jumping when launched via a direct level argument (--level ...). | Bypasses early title and cutscene state machines via direct engine CLI arguments while exercising 3D physics and AGC rendering. | | Gran Turismo 7 | Experimental branch (19736b1, Windows) | Reaches menus, garage view, and Music Rally / AutoDrive sequences, while visual corruption and instability remain open issues. | Illustrates the gap between shader opcode decoding (1,166 / 1,166) and full AGC render-target and barrier handling in a custom engine. | | Dead Cells (PPSA15552, v01.000.000) | Dedicated game fork (OverkillLabs3/DC-PS5, Windows 11) | Playable sessions with music, SFX, and keyboard/gamepad controls; rumble, lightbar, and motion remain unimplemented. | Shows why community videos must be checked for repository provenance: game-specific forks carry custom runtime changes not merged into stock boykopovar/AnyPS5. | Because behavior shifts across commits and branches, the AnyPS5 game checker records each report as a separate configuration card tied to its branch, commit hash, operating system, launch route, and eight verification checks (Launch, Menu, Gameplay, Finish, Audio, Inputs, Saves, Stability). 6. Building and testing AnyPS5 locally To inspect the codebase or test a clean, decrypted ELF that you have the legal right to use: 1. Match your release artifacts: Pair relinker-v0.1.2 with the matching prx-windows-v0.1.2.zip or prx-linux-v0.1.2.tar.gz archive from the same tag, or build both the relinker and libs CMake targets from the same commit (cmake --build build --config Release --target relinker --parallel and --target libs). 2. Verify host hardware and OS limits first: Check your Vulkan 1.1 driver, host RAM plus pagefile commit margin (up to 13,824 MiB of direct memory on Windows), and /dev/udmabuf permissions on Linux against the AnyPS5 requirements reference. 3. Keep libs/ and app0/ separated: Place AnyPS5's host .prx libraries (plus libgcc_s_seh-1.dll, libstdc++-6.dll, and libwinpthread-1.dll on Windows) inside libs/, and place converted guest modules inside app0/ at the relative paths printed by Guest module: during relinking. Step-by-step commands are documented in the AnyPS5 setup and AnyPS5 GitHub guides. References and primary sources 1. Upstream architecture specification (boykopovar/AnyPS5): docs/dev/ARCHITECTURE.md (commit a48c0066), covering the static relinker pipeline, nid_patcher symbol rewriting, __aps5_guest_initialize deferred Windows initialization, FreeBSD-modeled pthread cancellation, APR/AMM workers, and the 5-stage shader recompiler. 2. Upstream progress scanner rules (boykopovar/AnyPS5): docs/dev/PROGRESS.md (commit 43349d43), defining how tools/progress.py counts APS5_VABI definitions, NotImplemented_nid_no_patch stubs, shared_sources deduplication, and RdnaDecoder.cpp instruction recognition. 3. Upstream technical debt and SPIRV-Tools patch (boykopovar/AnyPS5): docs/dev/TechnicalDebt.md (commit a48c0066) and KhronosGroup/SPIRV-Tools Pull Request #6962. 4. Zak Killian, Tom's Hardware (Oct 10, 2026): AnyPS5 project reaches critical GPU milestone in race to enable running PS5 games natively on PC. 5. WhyCry, VideoCardz (Oct 9, 2026): AnyPS5 reaches 100% GPU shader instruction coverage for PS5-to-PC conversion. 6. TechPowerUp (Oct 2026): AnyPS5 Achieves Full PS5 GPU Shader Instruction Translation. 7. GIGAZINE (Oct 9, 2026): AnyPS5 is an open-source tool that allows you to play PS5 games on Windows and Linux. 8. AMD RDNA 2 Instruction Set Architecture Reference Guide: AMD GPUOpen / TechDocs (rdna2-shader-instruction-set-architecture.pdf) and AMD GPUOpen RDNA 2 Overview. 9. FreeBSD libthr pthread cancellation implementation: lib/libthr/thread/thr_cancel.c and lib/libthr/thread/thr_exit.c#L196-L259. 10. Khronos Vulkan and SPIR-V specifications: Khronos Vulkan Registry and Khronos SPIR-V Registry.
@brice_deg BTW I am trying to get a real KHR_lightmap extension so that people can stop baking light maps into glTF emissive maps in Blender. If you have thoughts, would love to hear them here: glTF#2679
glTF lightmap extension proposal, yay glTF#2679