Compare commits

..

607 Commits

Author SHA1 Message Date
Dean f40b6930ff
Merge pull request #4914 from MarcZX100/codex/carry-link-follow-symlink-4026
fix(libkernel_sys): follow symbolic links in link
2026-10-14 17:05:05 +13:00
Dean 00f4b3351b
Merge pull request #3972 from semirkabir/feat/videoout-output-status-vrr
feat(videoout): implement sceVideoOutGetOutputStatusWithVrr
2026-10-14 16:44:42 +13:00
Dean a31043146e
Merge pull request #5367 from MarcZX100/marc/sysctl-leaf-enotdir-5356
fix(libkernel): return ENOTDIR for trailing sysctl leaf components
2026-10-14 16:41:21 +13:00
Dean a1ca1e7bad
Merge pull request #5137 from MarcZX100/codex/request-v21-compat
fix(shader): accept both version 21 request layouts
2026-10-14 16:41:11 +13:00
Dean 21def5c6f8
Merge pull request #5597 from JJenkx/fix/thread-stack-retirement-order
fix(libkernel): retire stack metadata before releasing mappings
2026-10-14 16:41:01 +13:00
ThatsLeo 9828710a38 feat(videoout): implement sceVideoOutGetOutputStatusWithVrr
Writes the 80-byte status described in #2746: the sceVideoOutGetOutputStatus
fields followed by zeros, with the documented codes for a null status and an
unknown handle.

(cherry picked from commit e893efad2d0a7b90243d07bf6669cc9b5f540432)
2026-10-14 16:37:40 +13:00
Dean a14ae8e232
Merge pull request #5186 from p3n1cillin/claude/carry-fioclex
fix(libkernel): handle FIOCLEX and FIONCLEX in ioctl
2026-10-14 16:31:08 +13:00
Dean a5f493f90d
Merge pull request #5152 from dev-mca/test/convert-keycode
test(libSceConvertKeycode): cover sceConvertKeycodeGetImeKeyboardType
2026-10-14 16:06:42 +13:00
Dean f5cda839ec
Merge pull request #4717 from Ondra97/codex/fix-relro-unused-filter
fix(relinker): map standalone RELRO in unused import filtering
2026-10-14 16:05:01 +13:00
Dean e1544df5f2
Merge pull request #4772 from TideOPL/perf/spirv-specialization
perf(shader): index specialization tables by SPIR-V id
2026-10-14 16:04:50 +13:00
Dean f265cd5593
Merge pull request #5234 from AnthonyClemens/fix/agc-integer-tessellation-partitioning
fix(agc): accept integer tessellation partitioning
2026-10-14 15:55:50 +13:00
Dean 970316ae04
Merge pull request #4704 from AlxcBTW/feat/input-mouse-look
feat(input): turn mouse speed into a smooth right-stick tilt
2026-10-14 15:55:08 +13:00
Dean 486420f91a
Merge pull request #4950 from MarcZX100/codex/pr-overlap-compatible-merge-base
fix(tools): use the shared merge base for overlap checks
2026-10-14 15:54:50 +13:00
Dean 60ad9ad47c
Merge pull request #4951 from Ondra97/codex/fix-shared-library-import-audit
fix(tools): audit stubs from shared library sources
2026-10-14 15:54:35 +13:00
Dean fcb30fd977
Merge pull request #5219 from pushkarreddyy/feat/agc-dcb-event-write-address
feat(libSceAgc): ignore address argument on non-sample-dump events in sceAgcDcbEventWrite
2026-10-14 15:54:22 +13:00
Dean a8cb091eba
Merge pull request #4955 from Ondra97/codex/fix-high-refresh-skip-policy
fix(tools): accept skips on displays without 120 Hz
2026-10-14 15:54:20 +13:00
Dean 39bf2b9e9d
Merge pull request #4986 from Ondra97/codex/fix-conventions-raw-literals
fix(conventions): ignore C++ raw literal contents
2026-10-14 15:54:04 +13:00
Dean 0fb2f6c89a
Merge pull request #4988 from babyhuey/feat/scenet-so-error
fix(libSceNet): report SO_ERROR from sceNetGetsockopt
2026-10-14 15:53:49 +13:00
Dean 3e4fe4ce31
Merge pull request #5596 from tibzejoker/port/pr-637-main
fix(libc): track writes to singly mapped direct memory without faults on Windows
2026-10-14 15:53:44 +13:00
Dean 0a56aaaf47
Merge pull request #5284 from MarcZX100/codex/nested-exit-5277
fix(libc): finish exit handlers after nested exit
2026-10-14 15:50:18 +13:00
Dean d1b3aa1372
Merge pull request #5195 from SandstromPL/fix/relinker-sce-code-analysis-extent
fix(relinker): keep code analysis inside the code extent of PS4 executables
2026-10-14 15:49:02 +13:00
Dean a7836b1495
Merge pull request #5094 from bubblegg/fix/savedata-save-size
fix(savedata): report a save's size as the mount that created it gave
2026-10-14 15:47:32 +13:00
Dean aa1927872f
Merge pull request #5197 from AlexOfTheForest/test/relinker-cli-module-and-target-options
test(cli): cover module-dir, sce-module-path and target option errors
2026-10-14 15:46:50 +13:00
Dean 094a34c1ab
Merge pull request #5158 from AaravMehta-07/codex/systemservice-contained-exec
fix(libSceSystemService): contain converted executable paths
2026-10-14 15:45:40 +13:00
Dean 94f96c8e2c
Merge pull request #5255 from Nesyth/fix/libkernel-apr-read-past-eof-v2
fix(libkernel): zero-fill APR reads that reach the end of the file
2026-10-14 15:44:14 +13:00
Dean a21d73733c
Merge pull request #5133 from SandstromPL/fix/savedata-search-other-title
fix(savedata): return no saves for a search with another title's ID
2026-10-14 15:41:58 +13:00
Dean a1e1b4d161
Merge pull request #5262 from NaitikJoshiPro/pr/relinker-executable-tls
feat(relinker): executable TLS relocations (TPOFF64, DTPOFF64, DTPMOD64)
2026-10-14 15:41:31 +13:00
Dean 0defec4386
Merge pull request #5199 from Zenodeon/conversion-diagnostics
fix(relinker): identify failed inputs and invalid dynamic values
2026-10-14 15:39:35 +13:00
Dean c693bd7566
Merge pull request #5190 from NumberGuan/codex/check-lsda-landing-addresses
fix(relinker): reject overflowing LSDA landing pad addresses
2026-10-14 15:39:00 +13:00
Dean 75ceccd5db
Merge pull request #4615 from MiniRML/fix/nid-elf-table-layout
fix(nid): validate ELF table layouts before patching
2026-10-14 15:38:11 +13:00
Dean 622fb542d2
Merge pull request #4764 from nikowolf123/fix/conventions-test-fixtures
fix(conventions): skip library test fixtures in the export checks
2026-10-14 15:37:56 +13:00
Dean 9b6555bd47
Merge pull request #4796 from xXPBSXx/feat/socket-options-sol-socket
fix(libkernel): report SO_ERROR and the other SOL_SOCKET options like FreeBSD
2026-10-14 15:37:41 +13:00
Dean cdea950b7e
Merge pull request #4825 from StefanBuc/prepare/elf-import-audit-20261012
fix(tools): validate empty ELF dynamic symbol table bounds
2026-10-14 15:37:26 +13:00
Dean 1e277eef6e
Merge pull request #4930 from MarcZX100/fix/videoout-frame-dump-linear
fix(videoout): dump linear display buffers
2026-10-14 15:37:10 +13:00
Dean 91e5f60880
Merge pull request #4822 from MarcZX100/fix/fopen-native-open-errors-4814
fix(libc): return native fopen errors
2026-10-14 15:36:11 +13:00
Dean 6d032559c3
Merge pull request #4789 from PrintOrb/fix/audioout-shared-sdl-init
fix(audioout): serialize SDL audio initialization for Dark Forces
2026-10-14 15:35:56 +13:00
Dean 2ba3147ac8
Merge pull request #4844 from johnmaia/debug/skip-shader
feat(agc): skip the draws and dispatches of one shader with APS5_SKIP_SHADER
2026-10-14 15:32:04 +13:00
Dean 803cbf2f81
Merge pull request #4715 from Lorkydey/fix/agc-inactive-vertex-point-size
fix(agc): accept inactive vertex point size on non-point draws
2026-10-14 15:31:48 +13:00
Dean c5f839f651
Merge pull request #4702 from tv7/feat/gs-fast-launch
feat(agc): decode GS_FAST_LAUNCH geometry with one point per subgroup
2026-10-14 15:31:33 +13:00
Dean 0df6e295c5
Merge pull request #290 from MichonGoddijn231849/split/host-import-windows
feat(agc): host import windows, least-recent eviction under the budget
2026-10-14 15:27:19 +13:00
Dean 71c71f50b7
Merge pull request #5029 from fdfawef324/perf/agc-partially-resident-snapshot-hits
perf(agc): trust write stamps for snapshots of partially committed textures
2026-10-14 15:21:38 +13:00
pushkarreddyy fbf619ef0d
feat(libSceAgc): ignore address argument on non-sample-dump events in sceAgcDcbEventWrite 2026-10-14 07:37:02 +05:30
Dean 016db923af
Merge pull request #5184 from p3n1cillin/claude/sysctl-vm-overcommit
feat(libkernel): report vm.overcommit through sysctlbyname
2026-10-14 14:52:25 +13:00
AlxcBTW c29e06c693 fix(agc): a joined host import may replace imports the build in flight already holds
A join runs before any region confirms an import and a retired import lives
through the retire-hold, so refusing it for held overlaps kept the joined run
copied for the rest of the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:51:48 +13:00
Michon c871b5893d test(agc): host import windows, whole-range builds and budget refusals
agc_driver_graphics (BdaResources.cpp), each failing without its change:
- a view in the last window of a range whose first window was imported
  binds through an import of the whole range;
- an address-based build over the budget leaves the range's window in
  place (3 builds, same import);
- two uses across neighbouring cell edges share one import over 10
  rounds;
- a range mapped at the budget is refused at map time and retires
  nothing.
agc_driver_host_import_windows: windows follow their range, and
ClearHostImports drops them.
2026-10-14 14:51:48 +13:00
Michon e9249a0a47 feat(agc): host import windows, least-recent eviction under the budget
A registered range larger than 64 MiB is imported one 64 MiB window at a
time for buffer and fill accesses instead of whole, and the live imports
stay under a budget: APS5_HOST_IMPORT_MIB, else the larger of 6 GiB and
half the physical memory, at most the largest host heap less 2 GiB
(imports and type-3 mirrors share it; #167's rule). An import past the budget
retires the least recently used imports not used in the last 256
lookups.

- An access across window edges imports the cells it touches, grown to
  the union of the range's imports it overlaps, so uses across
  neighbouring edges share one import instead of replacing each other.
- An address-based build (AcquireRegistered) imports the range whole; a
  window over its first byte does not serve it.
- Every refusal (alignment, a failed run, a held import, the budget with
  what may be evicted) is decided before anything is retired.
- An import made when a range is mapped never evicts: the map observer
  holds no GPU lock.

The window size and the budget have setters for the tests.
2026-10-14 14:51:48 +13:00
Dean a98d42d08c
Merge pull request #4799 from ArjenS-LA/fix/trace-summary-selection
fix(tools): match explicit trace summaries to their capture
2026-10-14 14:51:27 +13:00
Dean 52d134c028
Merge pull request #4965 from pushkarreddyy/feat/agc-cmask-skip-higher-mip
feat(libSceAgcDriver): skip CMASK decode on single-sample views with viewMip > 0
2026-10-14 14:46:19 +13:00
Dean eacadec269
Merge pull request #4861 from Tamalero/fix/agc-metadata-pass-pass-through-depth
fix(agc): accept a CB metadata pass whose depth test passes every fragment without writing
2026-10-14 14:46:14 +13:00
Dean 959d39fb2b
Merge pull request #4801 from ArjenS-LA/fix/guest-posix-runpath
fix(relinker): validate Linux guest runpaths with target syntax
2026-10-14 14:45:57 +13:00
Dean 4586fdbc3b
Merge pull request #4469 from arcwiser/feat/libc-signal-list
feat(libSceLibcInternal): export sys_siglist and sys_nsig
2026-10-14 14:45:41 +13:00
p3nicillin dad29cac43 feat(libkernel): report vm.overcommit through sysctlbyname
The mimalloc in PS5MC (PPSA99420) reads vm.overcommit at start-up and
stopped on the unsupported name. It reports FreeBSD's default, 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:44:48 +13:00
Dean dff25dd604
Merge pull request #5098 from p3n1cillin/claude/sysctl-hw-realmem
feat(libkernel): report hw.realmem through sysctl
2026-10-14 14:44:32 +13:00
Dean 5a81f6085b
Merge pull request #4765 from nikowolf123/fix/conventions-template-subsections
fix(conventions): keep #### subsections inside the pull request template sections
2026-10-14 14:41:00 +13:00
Dean 1633843382
Merge pull request #4998 from ideabsassistenza/fix/debt-resolve-leading-entries
fix(debt): preserve leading entries during conflict resolution
2026-10-14 14:39:06 +13:00
Dean e5a272d7a6
Merge pull request #5359 from thuanquang/fix/output-path-directory
fix(relinker): reject a directory as the executable output path
2026-10-14 14:36:09 +13:00
Dean 3881dac903
Merge pull request #5148 from AaravMehta-07/codex/reject-relocatable-input
fix(relinker): reject relocatable ELF conversion inputs
2026-10-14 14:35:53 +13:00
Dean 54c2e14dc0
Merge pull request #5026 from AaravMehta-07/codex/reject-invalid-load-segments
fix(relinker): reject load file sizes beyond memory size
2026-10-14 14:35:37 +13:00
Dean e41cb23415
Merge pull request #5301 from wachi35676/fix/relinker-resource-path-spaces
fix(relinker): build the manifest resource in paths with spaces
2026-10-14 14:35:21 +13:00
Dean 52686e7959
Merge pull request #5300 from David-cmrg/fix/tests-ampr-cpp-error-pie
fix(tests): use position-independent unwind data in the C++ error fixture
2026-10-14 14:35:05 +13:00
Dean 4ddce7993d
Merge pull request #4845 from EagleBearerYS/test/guest-udp-readv-compare
test(libkernel): compare the unterminated readv buffer with memcmp
2026-10-14 14:34:41 +13:00
Dean 3e485ed53c
Merge pull request #4996 from MarcZX100/codex/mimg-sampler-vcc-4991
fix(shader): accept MIMG sampler tuples ending in VCC
2026-10-14 14:34:27 +13:00
Dean b9e0e4ed30
Merge pull request #5149 from xXPBSXx/fix/agc-texture-snorm-sint-2101010
fix(agc): sample 10_10_10_2 SNorm and SInt textures
2026-10-14 14:34:11 +13:00
Dean 97fce094c2
Merge pull request #5101 from Zheorgyan/fix/macos-import-lost-gpu-writes
fix(agc): make host imports over writable pages on macOS
2026-10-14 14:33:55 +13:00
Dean a87019d4ce
Merge pull request #4862 from Tamalero/fix/agc-blend-bypass-leaves-blending-off
fix(agc): leave blending off for a color target with CB_COLOR_INFO.BLEND_BYPASS
2026-10-14 14:33:39 +13:00
Dean 11b24ee816
Merge pull request #4863 from babyhuey/feat/libscevideoout-busy-open
fix(libSceVideoOut): return RESOURCE_BUSY when a bus is opened twice
2026-10-14 14:33:15 +13:00
Dean 8a437941bd
Merge pull request #4876 from EagleBearerYS/fix/savedata-failed-mount-keeps-result
fix(savedata): leave the mount result untouched when sceSaveDataMount3 fails
2026-10-14 14:32:59 +13:00
Dean 095a653c85
Merge pull request #5322 from lubaxe/fix/npauth-first-poll-running
fix(libSceNpAuth): report an async request as running on its first poll
2026-10-14 14:32:44 +13:00
Dean c5038c719f
Merge pull request #5170 from 0xRickx/fix/libscekeyboard-idle-count
fix(libSceKeyboard): report one key slot when no key is held
2026-10-14 14:32:30 +13:00
Dean 34355eeeac
Merge pull request #4942 from QRMOE/fix-libc-llroundl-upper-bound
fix(libc): accept the full llroundl integer range
2026-10-14 14:29:14 +13:00
Dean dd7174423a
Merge pull request #5009 from p3n1cillin/claude/libc-tzname
feat(libc): export tzname
2026-10-14 14:27:27 +13:00
Dean 0af3f150fa
Merge pull request #5153 from Kikoarl/feat/ces-sjis-cp932-profile-init
feat(ces): implement sceCesUcsProfileInitSJis1997Cp932
2026-10-14 14:27:11 +13:00
Dean 1535e2c688
Merge pull request #5414 from mariusyo07/feat/libc-mspace-aligned-alloc
feat(libc): implement sceLibcMspaceAlignedAlloc
2026-10-14 14:26:47 +13:00
Dean 1ebf8410a9
Merge pull request #5351 from MarcZX100/marc/fix-pathname-sysctl-eperm-5340
fix(libkernel): return EPERM before validating pathname sysctl MIB
2026-10-14 14:26:31 +13:00
Dean 2c444bb9fb
Merge pull request #5241 from MarcZX100/fix/sigwait-error-return-5224
fix(libkernel): return error numbers from sigwait
2026-10-14 14:26:15 +13:00
Dean 488ccece53
Merge pull request #4980 from MarcZX100/codex/poll-precise-wait-1471
fix(libkernel): use precise waits for empty poll sets
2026-10-14 14:26:00 +13:00
Dean 2e7ab67e8d
Merge pull request #4917 from MarcZX100/codex/carry-directory-read-4912
fix(libkernel): fail reads on Windows directory descriptors
2026-10-14 14:25:43 +13:00
Dean 7937c97237
Merge pull request #4923 from MarcZX100/codex/carry-ftruncate-not-file-4921
fix(libkernel): reject ftruncate on non-file Windows handles
2026-10-14 14:25:27 +13:00
Dean 8a51486bd2
Merge pull request #5393 from yixx16/fix/libkernel-vnode-windows-delete
fix(libkernel): keep the Windows vnode watch when the watched file is deleted
2026-10-14 14:25:11 +13:00
Dean d1c4618729
Merge pull request #4864 from milescoviello/carry/readv-pipe-windows-4858
fix(libkernel): fill every buffer of readv from a pipe on Windows
2026-10-14 14:24:56 +13:00
Dean c101c91bbb
Merge pull request #5272 from milescoviello/carry/siglongjmp-stack-windows-5270
fix(libkernel): switch the host stack bounds in siglongjmp on Windows
2026-10-14 14:24:40 +13:00
Dean 979a4b9bb7
Merge pull request #5464 from milescoviello/fix/kernel-stat-null-buffer
fix(libkernel): fail sceKernelStat with EFAULT for a null buffer
2026-10-14 14:24:24 +13:00
Dean c6926c9c00
Merge pull request #4958 from milescoviello/carry/flock-advisory-windows-4957
fix(libc): make flock advisory on Windows so other descriptors can read and write
2026-10-14 14:24:08 +13:00
p3nicillin fe54938490 feat(libkernel): report hw.realmem through sysctl
The dolphin_libretro core of RetroArch (PPSA99169) reads {CTL_HW,
HW_REALMEM} as the machine's memory size and stopped on the unsupported
MIB. It reports the same value as hw.physmem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:23:55 +13:00
Dean fc1e388ed2
Merge pull request #5185 from p3n1cillin/claude/sysconf-clk-tck
feat(libkernel): report _SC_CLK_TCK through sysconf
2026-10-14 14:22:06 +13:00
arcwiser 8e0d6324af docs: group signal list debt with signal handling 2026-10-14 14:16:05 +13:00
screamm 9849a1db64 feat(libSceLibcInternal): export sys_siglist and sys_nsig
PS5CEMU-HAR (PPSA99360, ps5-payload-dev build, relinks with
--trap-sdk-syscall-stubs) imports the data objects sys_siglist
(yCdGspbNHZ8) and sys_nsig (ZD+Dp+-LsGg) from libSceLibcInternal.prx;
the import audit lists both as absent (registry entries 364 and 378,
after pthread_setaffinity_np at 276). No run has reached them yet; the
predicted stop line is

  FAIL: unresolved ELF import ZD+Dp+-LsGg (sys_nsig) from libSceLibcInternal.prx

or yCdGspbNHZ8 (sys_siglist), whichever the loader resolves first.

The table and count are FreeBSD 11.4 lib/libc/gen/siglist.c (8c639945):
32 descriptions, signal 0 to SIGUSR2 (NSIG 32), and sys_nsig is their
number, 32. ps5-payload-dev declares both as objects exported by
libSceLibcInternal and its strsignal reads them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 14:15:55 +13:00
pushkarreddyy acbc573212 feat(libSceAgcDriver): skip CMASK decode on single-sample views with viewMip > 0 2026-10-14 14:15:25 +13:00
Cesar Rodriguez 8c554cc42a fix(agc): accept a CB metadata pass whose depth test passes every fragment without writing
DecodeColorMetadataPass refuses an eliminate-fast-clear or DCC-decompress
pass when DB_DEPTH_CONTROL has any of its low four bits set and depth or
stencil planes are bound. Marvel's Guardians of the Galaxy (PPSA01748)
issues one with the Z test enabled, ZFUNC ALWAYS and no Z write
(DB_DEPTH_CONTROL 0x72) while a Z32 and an S8 target are bound. That test
passes every fragment and leaves depth unchanged, so the pass does no
depth work. Draws already treat this state as a pass-through
(depthPassThrough); the metadata pass now uses the same rule. Depth
bounds, depth writes, stencil tests and DB_RENDER_CONTROL clears or copies
are still refused, and the error names the four registers involved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:15:04 +13:00
ArjenS-LA 394c5a231e fix(relinker): validate Linux guest runpaths with target syntax 2026-10-14 14:14:38 +13:00
Dean ee9037d851
Merge pull request #4694 from EagleBearerYS/fix/agc-draw-snapshot-cap-at-build
perf(agc): read the draw snapshot cap when resources are built
2026-10-14 14:14:16 +13:00
Dean cd90ca50be
Merge pull request #4712 from Ondra97/codex/fix-debt-section-moves
fix(tools): preserve technical debt section moves
2026-10-14 14:13:58 +13:00
Dean b16a1a72f3
Merge pull request #4776 from babyhuey/feat/libkernel-sysctl-tsc-freq
feat(libkernel): report machdep.tsc_freq from sysctlbyname
2026-10-14 14:09:31 +13:00
Dean 19785b50e9
Merge pull request #4425 from Lorkydey/fix/import-audit-atomic-json
fix(import-audit): preserve JSON reports when writing fails
2026-10-14 14:06:16 +13:00
Dean d7bb2fee3d
Merge pull request #5580 from oneandonlydean/fix/videoout-dinput-1042
fix(videoout): disable SDL DirectInput joystick enumeration on Windows
2026-10-14 14:02:29 +13:00
Dean 604ce71864
Merge pull request #4947 from Kikoarl/perf/agc-draw-recipe-depth-expansion
perf(agc): expand draw recipes to support depth targets
2026-10-14 14:00:34 +13:00
Dean 1f1115dc90
Merge pull request #4795 from MarcZX100/codex/pipeline-cache-snapshot-lock-4781
fix(agc): serialize pipeline cache snapshots
2026-10-14 14:00:21 +13:00
Dean 0dea07ab27
Merge pull request #4517 from AlxcBTW/perf/host-import-join-exact
perf(agc-driver): join the imports of adjacent ranges of any size that a buffer is bound exactly over
2026-10-14 13:53:00 +13:00
thibaut b7d7e71bf6 docs(libc): move the Windows direct memory debt entry to libc.md
The entry links WindowsMappings.hpp, so the debt-file convention puts it in
docs/dev/debt/libc.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
2026-10-14 02:39:10 +02:00
thibaut f32a9407e4 test(libc): require resident direct memory in the resident run tests
CheckBreakingAResidentRunKeepsConcurrentAccesses and
CheckRemappedDirectMemoryIsChargedOnce passed on main, where a singly
mapped direct memory page is a shared view and no resident run exists.
Both now require their first mapping to be private committed memory, so
on main they fail there instead of passing without exercising a run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
2026-10-14 02:39:10 +02:00
thibaut 71da166a18 fix(libc): keep resident mappings to direct memory backings on Windows
Map made every singly mapped section page private resident memory. A JIT
or shm_open section is file backed and committed: its bytes live in the
file, and its alias descriptors map duplicated handles, which the physical
page slots do not match, so a resident copy diverged from the file, and
committing its view on a demote failed with access denied
(guest_jit_shared_memory). Only direct memory backings, mapped through the
new GuestArenaMapDirect, now become resident; other sections keep main's
shared views. Shared pages are committed only while their section page is
reserved, as on main.

The socket host write tests map their direct memory a second time, so the
write-protected shared path they check is still the one they exercise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
2026-10-14 02:39:10 +02:00
DotDebian d9852df2a2 fix(libc): track writes to singly mapped direct memory without faults on Windows
A direct memory page mapped at one guest address now lives in private
MEM_WRITE_WATCH memory at that address, and its writes are collected with
GetWriteWatch like flexible memory: no fault is raised. A page mapped at a
second address moves its whole resident run into the section, behind a
read-only fence while it is copied, and every address becomes a view of it,
tracked by write protection. Unmapped resident pages that are not blank are
kept in a private reservation until they are mapped again, not in the
section.

Ported onto main's 4 KiB subpage tracking of shared views: resident runs
keep a per-page collected bit and enter the fresh ranges, so Fresh and
Armed collections select them like shared views; a demoted run carries its
collected bits into the views and reports every page once. MapAlias throws
through refuse() when a resident run extends outside the aliased range, and
the caller keeps main's fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
2026-10-14 02:39:10 +02:00
MarcZX100 4307e3cb54 fix(agc): serialize pipeline cache snapshots 2026-10-14 13:34:43 +13:00
AlxcBTW d275a58814 fix(agc-driver): copy a region an exact join's import covers only in part
After an exact two-piece join, a bind across the join's end got the
joined import from importAllocation and the view failed as "rounded up to
its view range exceeds its GPU owner". UploadFinish now asks importJoined
for a region past the import's end and drops an import that still does
not cover the region, so the region is copied, as on main. The join test
binds across the end of an exact join after a third piece is registered.

Co-authored-by: TideOPL <57710757+TideOPL@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:34:31 +13:00
AlxcBTW ca95f74097 perf(agc-driver): join the imports of adjacent ranges of any size that a buffer is bound exactly over
A bound buffer is joined across adjacent registered ranges only when every
one of them is larger than the 8 MiB window. PPSA25344's loading binds 16 MiB
and 4 MiB buffers whose backing was registered in two to six adjacent
pieces with the same access, some of them smaller than the window, so each
use of such a buffer was copied on the CPU: up to 348 GB in the first 300 s
of loading.

A bound buffer that starts at the first crossed range's start and ends at the
last one's end now joins those ranges whatever their size. A heap grown in
pieces matches that only for a binding over exactly its current pieces, so
growth beside a joined import does not re-import it. joinedCovers, used when a
joined import retires, accepts any adjacent run of ranges.

importJoinTests binds exactly over two 64 KiB pieces of a partly filled
window and requires one import of both; a binding across them that starts
inside the first piece stays apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:34:31 +13:00
Dean ce5c839533
Merge pull request #4883 from nikowolf123/perf/shader-cache-resource-plan
perf(recompiler): keep the front-end resource plan in the shader disk cache
2026-10-14 13:33:01 +13:00
Dean 29d5f067b9
Merge pull request #5003 from tibzejoker/feat/depth-storage-array-views
feat(agc): storage image access to depth planes through 2D array views
2026-10-14 13:32:41 +13:00
Dean af991ac404
Merge pull request #4747 from TideOPL/perf/bc-block-copy
perf(agc): copy a pending block-compressed surface into its sampled texture on the GPU
2026-10-14 13:32:04 +13:00
Dean cb320fd884
Merge pull request #4603 from AlxcBTW/perf/agc-memory-value-fill
perf(agc): take the memory-value dword fill kernel as a buffer fill
2026-10-14 13:31:43 +13:00
Dean 520ee1a2e8
Merge pull request #4490 from karanvora2599/perf/shader-minmax-floor-denormal-free
perf(shader): treat f32 min, max, med3, floor and ceil results as denormal-free
2026-10-14 13:31:23 +13:00
Dean afc8541d18
Merge pull request #4487 from Pipodi/feat/agc-forced-srgb-views
feat(agc): sample non-sRGB textures through FORCE_DEGAMMA samplers as sRGB views
2026-10-14 13:31:02 +13:00
Dean 7cc6cd8633
Merge pull request #4465 from mrzacarias/fix/agc-buffer-pattern-fill
fix(agc): handle the fill kernel that reads its pattern from a buffer
2026-10-14 13:30:42 +13:00
JJenkx d1527840ea fix(libkernel): retire stack metadata before releasing mappings 2026-10-13 20:30:24 -04:00
fdfawef324 8d8fe39eed fix(agc): flush pending stores before a partial snapshot hit
A partially resident snapshot cannot be reused from committed write stamps while a recorder batch still owns an overlapping completion store. Make that case compare through the guest-memory flush hook before deciding whether the snapshot is unchanged.

Cover copied completion stores in both open and submitted batches, then verify that unchanged snapshots are reused and an unrelated pending write does not synchronize. The focused regression fails on the published PR head and passes with the guard. The existing fully committed snapshot path is unchanged.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-13 20:25:46 -04:00
fdfawef324 955b56a2b5 perf(agc): trust write stamps for snapshots of partially committed textures
A sampled texture over a partially resident surface (a texture array whose
heap the guest commits on demand) never got a write generation: CollectWrites
fails on a range with reserved pages, so every cache hit compared the whole
committed part with its snapshot. CollectCommittedWrites collects the
committed runs one by one and returns the runs with the generation; the cache
entry records them, and a hit skips the compare only when the committed runs
are the same and UnchangedCommittedSince holds over them. A commit, decommit,
view mapping or unmapping inside the surface changes the runs or stamps the
new pages, so it is compared as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:25:46 -04:00
Dean Galvin 56d1eda2f0 docs(videoout): record disabled DirectInput controller backend
Co-authored-by: Tenith01 <102300949+Tenith01@users.noreply.github.com>
2026-10-14 13:14:15 +13:00
Tenith01 e94f12bd17 fix(videoout): disable DirectInput joystick enumeration on Windows 2026-10-14 13:14:15 +13:00
Dean feddf7f4c6
Merge pull request #4849 from MarcZX100/codex/fix-address-cache-shutdown-4846
fix(agc): release cached address space on device teardown
2026-10-14 13:13:08 +13:00
Dean 014f3623a7
Merge pull request #4508 from jradelmo/feat/flexible-memory-budget
fix(kernel): honor the declared flexible memory budget
2026-10-14 13:13:03 +13:00
Kikoarl 3ebad9045e perf(agc): expand draw recipes to support depth targets 2026-10-14 02:00:40 +02:00
David-cmrg 75c25e16f0 fix(tests): use position-independent unwind data in the C++ error fixture 2026-10-14 01:49:52 +02:00
Dean d1670009fb
Merge pull request #5429 from Dripl1x/perf/windows-mutex-wake
perf(libkernel): wake guest mutex waiters on release on Windows
2026-10-14 12:31:24 +13:00
Evgeniy Kazarin 99eaf73102 perf(libkernel): wake guest mutex waiters on release on Windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:18:33 +03:00
Adelmo Junior 12851ef037 docs: document flexible memory budget behavior 2026-10-13 20:13:14 -03:00
Adelmo Junior 72211b70fe fix(kernel): honor the declared flexible memory budget
Read the optional aligned budget from param.json and apply it consistently to configured, available and mapped flexible memory. Add parser and allocation regression coverage.
2026-10-13 20:13:14 -03:00
Dean 7a0c838ca4
Merge pull request #4875 from nikowolf123/perf/libc-write-watch-capacity
perf(libc): size each write-watch query to the region it covers
2026-10-14 12:12:10 +13:00
AlxcBTW 6ffc99bedb perf(agc): take the memory-value dword fill kernel as a buffer fill
The 9-word kernel #3816 matches stores one dword per thread, loaded with
s_buffer_load_dword through the descriptor in user[4..7]. Run as a
compute dispatch, its stores are pending writes of unknown value, so a
later read of DCC keys in that range waits for the GPU.

fillBuffer now takes it as a uniform buffer fill when all of these hold:
- the output descriptor's format is 32_UINT, 32_SINT or 32_FLOAT (20-22),
  so buffer_store_format_x stores the loaded dword unchanged;
- the value descriptor covers the loaded dword: num_records times the
  stride (num_records alone when the stride is 0) is at least 4 bytes,
  the range the recompiler gives a scalar buffer load;
- exactly 8 user SGPRs and group id X enabled, so the group id is s8;
- a direct, non-partial dispatch, 64x1x1 threads, one group in Y and Z,
  and a 16-byte aligned range of whole 16-byte units.

The value is read when the fill is recorded, after the labels queued
before it, as the pattern-fill kernel's source is. When the value is one
byte repeated and that byte is a DCC key code, the fill goes into the GPU
key-store memo whatever ClassifyFill reports: the memo answers a key
read only while this fill is still the newest pending write over the
keys, and a fill over a color target's keys is not a storage image's
keys fill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:44:41 +02:00
David 773e7f218c docs(debt): describe the byte the Windows flock locks 2026-10-13 22:29:33 +00:00
David b962be8536 test(libc): read and write through another descriptor while a flock is held 2026-10-13 22:29:20 +00:00
David 32fdaa65ae fix(libc): make flock advisory on Windows so other descriptors can read and write 2026-10-13 22:29:20 +00:00
Dean 30f961266a
Merge pull request #4973 from ducloc080202/fix/cq3-positioned-io
fix(libkernel): serialize positioned file transfers on Windows
2026-10-14 11:28:14 +13:00
Dean 83924acb18
Merge pull request #4467 from arcwiser/feat/libc-swscanf
feat(libc): implement swscanf
2026-10-14 11:27:57 +13:00
Dean Galvin 296ff64ae9 fix(libkernel): share positioned transfer locks on Windows only
The per-descriptor lock exists only in the Windows transfer path, so ShareDescriptor calls SharePositionedTransfer under _WIN32 like GuestFileLockShare. Linux failed to link libkernel with an undefined reference to it.
2026-10-14 11:18:55 +13:00
ducloc080202 0b46b3080a test(libkernel): cover concurrent pread against seek and read 2026-10-14 11:18:07 +13:00
ducloc080202 3cab3c05ec fix(libkernel): use per-descriptor locks for positioned transfers and seeks 2026-10-14 11:18:07 +13:00
Anshika Gupta 3915f6dc09 fix(libkernel): serialize positioned file transfers on Windows 2026-10-14 11:18:01 +13:00
screamm 8aa0d42824 feat(libc): implement swscanf
swscanf (HNnWdT43ues) runs the swscanf_s scanner without the buffer
size arguments after %s, %c and %[, as FreeBSD's swscanf.c passes its
arguments to vswscanf. Both entry points share one engine with a flag
for the sizes; swscanf_s behaves as before.

Title: PS5CEMU-HAR PPSA99360 imports swscanf from libSceLibcInternal.prx
(import audit with --trap-sdk-syscall-stubs, registry index 292, right
after pthread_setaffinity_np at 276). Predicted stop line once
pthread_setaffinity_np resolves, without this change:
FAIL: unresolved ELF import HNnWdT43ues (swscanf) from libSceLibcInternal.prx

NID from tools/nid_names.py, confirmed in shadPS4 aerolib.inl:51950
(e0a466753a1a). Prototype in ps5-payload-dev include/freebsd/wchar.h:133,
export in sce_stubs/libSceLibcInternal.c:8582 (f5d40f02770e).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 11:17:04 +13:00
Dean bf8dc610e6
Merge pull request #5425 from SandstromPL/feat/ces-module-library
feat(libSceCesCs): build libSceCesCs-module from the libSceCesCs source
2026-10-14 11:14:19 +13:00
Dean 703aa8f63d
Merge pull request #5411 from ps54all/feat/libscenet-resolver-ntoa6
feat(libSceNet): implement sceNetResolverStartNtoa6
2026-10-14 11:12:27 +13:00
Dean bfc10637d4
Merge pull request #5216 from oneandonlydean/up4/draw-key-second-sight
perf(agc): cache a draw key the second time it is seen
2026-10-14 11:06:03 +13:00
Dean 65beff9b19
Merge pull request #5478 from oneandonlydean/up4/sdwa-ubyte-1014
fix(recompiler): accept SDWA source selects on v_cvt_f32_ubyte1, ubyte2 and ubyte3
2026-10-14 11:05:47 +13:00
Dean c7601b7971
Merge pull request #4164 from hamb3r/feat/libc-unity-il2cpp-runtime
feat(libc): add missing Unity IL2CPP runtime imports and Dinkumware helpers
2026-10-14 10:45:32 +13:00
thibaut 696d1660c1 feat(agc): storage image access to depth planes through 2D array views
A 2D array storage view of a depth plane from slice 0 threw. It now gets
a storage image over all its slices: each slice with a depth surface at
the plane's address plus a whole depth slice per layer is copied from
that surface before every use and back after the work; the other slices
hold guest memory like any storage image. Cubes, stencil arrays, later
base slices and layer pitches other than the depth slice size still
throw.

GTA V (PPSA04264) stops at compute shader 0x242d4a200 on a 5-slice R32
Z_64KB_X array over its shadow depth surfaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
2026-10-13 23:38:51 +02:00
Dean 665ad7da74
Merge pull request #4374 from MarcZX100/codex/carry-getpriority-3632
feat(libkernel): implement getpriority for the guest process
2026-10-14 10:36:42 +13:00
Hussein Kadhim 091438f99b fix(libkernel): fail sysctl with ENOTDIR when a leaf MIB has a trailing component
FreeBSD sysctl_find_oid returns ENOTDIR when the MIB continues below a leaf object. The supported hw.ncpu, hw.model, hw.physmem and kern.arandom MIBs threw an unsupported MIB error instead.

(cherry picked from commit 6968aea8af578c4d56dc614cbe449274bb6e76e7)
2026-10-13 21:33:25 +00:00
David 1ba0a81820 feat(libkernel): implement getpriority for the guest process
PS5MC (PPSA99420, Kodi 22) imports getpriority (miQ1wVwSexE) from
libkernel.prx, one reference, next to chflags in its import order.
Predicted stop once the imports ahead of it are covered (not a run):

  FAIL: unresolved ELF import miQ1wVwSexE from libkernel.prx

The NID is computed with tools/nid_names.py and matches aerolib.

Follows FreeBSD 11.4 sys/kern/kern_resource.c sys_getpriority
(8c639945a44588f6cfdddfb057bb5aad88735da0) for the only guest process,
taken to run at nice 0 and to lead its own process group (as kill and
setsid already assume): PRIO_PROCESS and PRIO_PGRP with 0 or its pid,
and PRIO_USER with 0 or its uid 0, return 0; other ids give ESRCH and
an unknown which gives EINVAL. getpriority is a bare system call in
FreeBSD's libc (lib/libc/sys/Symbol.map), so errno is left unchanged
on success.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 63262245d274342b693fda146ae9feca3752b6e3)
2026-10-14 10:23:41 +13:00
Dean 8d98e8851f
Merge pull request #4474 from JJenkx/fix/multisample-target-retirement
fix(agc): retire fully unmapped multisample targets
2026-10-14 10:23:12 +13:00
Dean cf5f5b53b5
Merge pull request #4379 from MarcZX100/codex/carry-chflags-3632
feat(libkernel): carry chflags implementation
2026-10-14 10:22:02 +13:00
Dean d638e02860
Merge pull request #3991 from Sanjaysrinivas/perf/agc-lazy-storage-snapshot
perf(agc): allocate storage CPU snapshots on demand
2026-10-14 10:21:23 +13:00
Dean 7d757bbf0a
Merge pull request #4383 from pushkarreddyy/feat/libkernel-sigpending-sigsuspend
feat(libkernel): implement sigpending
2026-10-14 10:21:01 +13:00
Dean ea2b2d364f
Merge pull request #4514 from kallmek3nny/feat/libkernel-pthread-setaffinity
feat(libkernel): implement pthread_setaffinity_np
2026-10-14 10:20:33 +13:00
Dean Galvin 7b2104ee0d fix(recompiler): accept SDWA source selects on v_cvt_f32_ubyte1, ubyte2 and ubyte3
v_cvt_f32_ubyte0 accepts every SDWA source selector, but the other three byte
conversions had no VOP1 SDWA rule, so any SDWA source select on them was refused
("VOP1 SDWA source selector 4 of opcode 0x12 at pc 0xfcc is not supported").
The selected and optionally sign-extended source goes through the same operand
path as for ubyte0 before byte N is extracted: SDWA supplies source 0 (RDNA 2 ISA
guide 6.8 and 13.3.7) and the opcode reads S0.u[8N+7:8N] (12.8).

agc_driver_sdwa_cvt_f32_ubyte: WORD_0 and WORD_1 selects on ubyte1, and
sign-extended WORD_1 and BYTE_3 selects on ubyte2 and ubyte3.
2026-10-14 09:36:10 +13:00
MarcZX100 b6a10e91aa fix(libkernel): remove duplicate chflags error constant 2026-10-13 20:28:23 +00:00
Dean 0aa4cb1c2f
Merge pull request #3798 from YossiYad/carry/posixforwebkit-getservbyname
feat(libScePosixForWebKit): implement getservbyname
2026-10-14 09:24:05 +13:00
milescoviello eb78c6a45f fix(libkernel): fail sceKernelStat with EFAULT for a null buffer
Crimson Desert (PPSA25286) calls sceKernelStat with a null buffer at
start-up, and main throws "sceKernelStat: sb is null", which ends the
title in its first second.

FreeBSD 11.4 looks the path up first and only then copies the result
out, so a null buffer gets EFAULT for a path that exists and the lookup
error for one that does not (sys/kern/vfs_syscalls.c, sys_stat):

	error = kern_statat(td, 0, AT_FDCWD, uap->path, UIO_USERSPACE,
	    &sb, NULL);
	if (error == 0)
		error = copyout(&sb, uap->ub, sizeof (sb));

sceKernelStat now does the same: the lookup and its errors are as
before, and a null buffer returns SCE_KERNEL_ERROR_EFAULT where the
status would be copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:20:07 +00:00
Dean a0f5916a29
Merge pull request #4417 from Jnbla6/fix/ngs2-rack-4001
feat(libSceNgs2.native): implement custom sampler rack id 0x4001 (#4334)
2026-10-14 08:55:20 +13:00
MarcZX100 852c808373 test(libSceNgs2): fix custom filter regression expectations 2026-10-14 08:47:31 +13:00
Zheorgyan 3960519b1e fix(agc): make host imports over writable pages on macOS
Co-authored-by: Marcio Rocha Zacarias <6743187+mrzacarias@users.noreply.github.com>
2026-10-13 21:43:21 +02:00
Dean Galvin 22f6b1735c docs: keep the technical debt index and end the NGS2 entry with a newline
The rebase onto main deleted docs/dev/TechnicalDebt.md, which is now the
index that points into docs/dev/debt/. Restore it from main and add the
missing final newline to docs/dev/debt/libSceNgs2.native.md.
2026-10-14 08:39:38 +13:00
jnbla6 40ebfae70b fix: throw on unsupported filter types and update debt docs 2026-10-14 08:39:32 +13:00
Jnbla6 ef52b16920 feat(libSceNgs2.native): add custom voice filter support and related tests for 0x13 issue case 2026-10-14 08:39:32 +13:00
Jnbla6 855823e85e feat(libSceNgs2.native): implement custom sampler rack id 0x4001 (#4334) 2026-10-14 08:39:32 +13:00
lorkydey d45fee9b69 fix(import-audit): preserve JSON reports when writing fails 2026-10-13 21:20:45 +02:00
mrfelek 48b69fad4d feat(libc): implement sceLibcMspaceAlignedAlloc 2026-10-13 21:20:32 +02:00
SandstromPL 299a736b54 feat(libSceCesCs): build libSceCesCs-module from the libSceCesCs source
Titles that link the character conversion library under its module name
need libSceCesCs-module.prx, which nothing built: Void Terrarium
(PPSA03061, #5055) and Call of Duty: Black Ops (PPSA34501, #5389) were
run with a copy of libSceCesCs.prx under that name. Both libraries are
now built from the same source by add_sce_ces_library, as libScePsml and
libSceFont-module are, and guest_ces_module runs the CesCs test against
the module-named library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:44:56 +05:30
ps54all df32a44f79 feat(libSceNet): implement sceNetResolverStartNtoa6
Resolve a host name to its first IPv6 address through the host resolver
(getaddrinfo with AF_INET6) and write the 16-byte address. The lookup
shares the IPv4 path's resolver bookkeeping (abort preservation, error
state); a name without an IPv6 address fails with RESOLVER_ENODNS like a
failed lookup. Nonzero flags throw.

Metal Gear Solid 4 Master Collection (PPSA18194) imports it and failed to
load without it (unresolved zl35YNs9jnI from libSceNet.prx).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:13:34 +02:00
MarcZX100 2ad825f659 fix(shader): require the version 22 F16 request flag 2026-10-13 19:09:49 +00:00
MarcZX100 0e087e974e test(shader): cover version 21 F16 request layout 2026-10-13 19:09:49 +00:00
MarcZX100 dffdb5002e fix(shader): read both version 21 request layouts 2026-10-13 19:09:49 +00:00
Stefan Bucataru 89a8ec58b1 fix(tools): validate empty ELF dynamic symbol table bounds 2026-10-13 21:07:36 +02:00
Dean dacd78525e
Merge pull request #4949 from fdfawef324/fix/agc-unwritten-sample-locations
fix(agc): start the sample location registers at their clear-state value
2026-10-14 08:03:16 +13:00
MarcZX100 dfc9223248 docs(libkernel): retain chflags debt after rebase 2026-10-13 18:54:49 +00:00
MarcZX100 8b5cfb9850 test(libkernel): declare chflags guest entry point 2026-10-13 18:54:07 +00:00
David 4dd97f7dbe feat(libkernel): implement chflags through the guest path resolver
PS5MC (PPSA99420, Kodi 22) imports chflags (ixrw0h2tWuI) from
libkernel.prx, one reference. Predicted stop once the imports ahead of
it (pthread_getthreadid_np, sendfile, wcstok, pause, setrlimit and the
termios calls) are covered (not a run):

  FAIL: unresolved ELF import ixrw0h2tWuI from libkernel.prx

The NID is computed with tools/nid_names.py and matches aerolib.

Follows FreeBSD 11.4 (8c639945a44588f6cfdddfb057bb5aad88735da0)
sys/kern/vfs_syscalls.c kern_chflagsat and setfflags, and
sys/ufs/ufs/ufs_vnops.c ufs_setattr, for a root caller: the lookup
through the guest resolver gives EFAULT, ENOENT and ENOTDIR (also for a
file reached through a trailing slash or used as a directory), a bit
outside the 17 UFS flags of sys/sys/stat.h (VNOVAL included) gives
EOPNOTSUPP, and SF_SNAPSHOT gives EPERM, since the snapshot flag cannot
be toggled. No host keeps BSD file flags and stat reports st_flags 0,
so flags 0 succeed without touching the file and any other flag throws
with the flags named. UF_READONLY and the other Windows-attribute flags
are not mapped to FILE_ATTRIBUTE_READONLY and its siblings: UFS stores
them without enforcing them, and on Windows the read-only attribute is
already the owner write bit of chmod.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 16e5b7ccd04e6467a5a282b6cfc9271304f42244)
2026-10-13 18:54:05 +00:00
Jesus 50b06110bb fix(libkernel): keep the Windows vnode watch when the watched file is deleted
Windows 10 1903 deletes a file with open handles by moving it into
$Extend\$Deleted until the last handle closes, so the next snapshot of a
watched file that a title (or the test) deleted saw a new path, tried to
watch that directory's changes, got ERROR_ACCESS_DENIED and threw from
the watch thread, which aborts the process; the path change was also
reported as NOTE_RENAME next to NOTE_DELETE. A snapshot with a pending
delete or no links now reports NOTE_DELETE alone and drops the parent
directory watch instead of replacing it, as the file has no directory
to be renamed or removed from any more.
2026-10-13 13:41:05 -05:00
fdfawef324 15955c089e fix(agc): start the sample location registers at their clear-state value
A draw with PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES > 0 whose context never
wrote PA_SC_AA_SAMPLE_LOCS_PIXEL_* (context 0x2fe-0x30d) was rejected
with "missing register in context bank at DWORD 0x2fe", because the
registers were absent from the initial context (PPSA07411 stops on it
after two frames). The MSAA_ENABLE single-sample path read them the
same way.

The sixteen registers now start at 0, both initially and after
CLEAR_STATE. That is the value in libSceAgc's own register defaults
(sceAgcGetRegisterDefaults2, every public table that holds context
registers, the tables the initial context already follows for
PA_SU_VTX_CNTL and PA_SC_CONSERVATIVE_RASTERIZATION_CNTL), in amdgpu's
GFX10 clear-state table (clearstate_gfx10.h, gfx10_SECT_CONTEXT_def_8,
loaded by gfx_v10_0_cp_gfx_start before CLEAR_STATE) and the GC 10.3
register reset value (gc_10_3_0_default.h). RADV, radeonsi and PAL
always write the locations themselves and do not settle the unwritten
value.

0 puts every sample at the pixel center. That is not the standard
pattern for any sample count, so such a draw decodes as custom
locations at the center, as a single-sample scan into a multisampled
target already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:06:05 -04:00
thuanquang 286cf28ec7 fix(relinker): reject a directory as the executable output path
An existing directory, or a path ending in a separator, was only
rejected when the executable was written, after the converted modules
had already been written to app0/ beside or inside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:49:48 +07:00
Dean 18c438b109
Merge pull request #4491 from xXPBSXx/fix/playgo-bound-no-chunk-table
fix(libScePlayGo): answer chunk ids from the package layout
2026-10-14 06:42:39 +13:00
Hussein Kadhim 2096410b29 fix(libkernel): report EPERM before EINVAL when writing kern.proc.pathname
FreeBSD sysctl_root rejects a write to a read-only node with EPERM before the node handler checks the MIB length, so a write with a short or long kern.proc.pathname MIB fails with EPERM. sysctlbyname for kern.proc.pathname now goes through sysctl with its 3-component MIB so it follows the same order.
2026-10-13 17:39:56 +00:00
Niko 5d3ea5f511 test(recompiler): set layerExported in the disk cache entry round trip
#5097 added ShaderInfo::layerExported to the entry encoder, and this
series moves that line into encodeShaderInfo, which plan entries use
too. The entry round trip left the flag false, so an encoder and a
decoder that both skipped it still passed. Set it like the other
ShaderInfo flags: with layerExported dropped from both, the round trip
now fails with "entry round trip: shader info differs".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:30:21 +02:00
boykopovar 8cf6a0bc3a
Merge pull request #5328 from boykopovar/chore/add-first-demo-collage
chore(readme): add games demo collage to readme
2026-10-13 20:18:47 +03:00
boykopovar bdded28693 chore(readme): add games demo collage to readme 2026-10-13 20:16:47 +03:00
Sasha Papo cb8b0caf7d fix(libSceNpAuth): report an async request as running on its first poll
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:07:19 -04:00
Dean 61b8742b6b
Merge pull request #4515 from AlxcBTW/perf/host-import-join
perf(agc-driver): join the imports of adjacent large ranges when a buffer is bound across them
2026-10-14 05:55:27 +13:00
Dean 9baec8793f
Merge pull request #3870 from yash27git/clean/Direct-Memory-Exception
fix(libkernel): harden DirectMemory validation and tests
2026-10-14 05:55:11 +13:00
Dean 40047f46bd
Merge pull request #4377 from MarcZX100/codex/carry-pthread-atfork-3628
feat(libkernel): implement pthread_atfork registration
2026-10-14 05:54:54 +13:00
Muhammad Wasif Ali Wasif 04421e7d4d fix(relinker): build the manifest resource in paths with spaces
windres passes include directories to the preprocessor unquoted, so
relinker.rc failed to compile when the checkout path contains a space.
The resource is now an object library without the relinker's include
directories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:43:59 +05:00
screamm 74c3fb6576 feat(libkernel): implement pthread_setaffinity_np
PS5CEMU-HAR (PPSA99360, ps5-payload-dev build, relinks with
--trap-sdk-syscall-stubs) imports pthread_setaffinity_np (5KWrg7-ZqvE)
from libkernel.prx. babyhuey's run with #3787 merged onto main e7c70a2d
(PR #3787, 2026-10-12T10:54:44Z) stops at it:

  Windows: FAIL: unresolved ELF import 5KWrg7-ZqvE (pthread_setaffinity_np) from libkernel.prx
  Linux: undefined symbol: 5KWrg7-ZqvE (pthread_setaffinity_np)

It follows FreeBSD 11.4 libthr thr_affinity.c (8c639945) over
kern_cpuset_setaffinity: another thread is looked up first (EINVAL for
null, ESRCH when unknown or finished, as _thr_find_thread without dead
threads), a size other than the 32-byte cpuset_t returns ERANGE, the
mask is clipped to sceKernelGetAvailableCpumask and an empty result
returns EDEADLK, as cpuset_setaffinity does. The affinity is stored on
the guest thread through scePthreadSetaffinity, for the caller and for
any live guest thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:39:07 +00:00
Niko 664ad77037 test(recompiler): pin the field counts of the structures the plan codec writes
The plan and ShaderInfo encoders write these structures field by field,
and the sizeof asserts are their only reminder to update the encoders. A
field that fits in existing padding keeps the size: main's layerExported
landed in ShaderInfo with sizeof still 232, and the three bools #5089
adds to DescriptorSource::IndirectImage keep it at 28. A warm cache
would then hand back entries without the new field and nothing would
fail.

Count the fields of every aggregate the plan codec reaches (IrResourcePlan,
GuestRegister, MemoryInfo, DescriptorSource and IndirectImage,
ResourceBlock, SrtRead, UniformFill, UniformFillPlan, and ShaderInfo with
its resource and stage I/O records) by brace-initializing each with more
and more convertible-to-anything values until the initializer no longer
compiles, and assert the counts. Unlike the size asserts this does not
depend on the ABI, so it is checked on every platform. IrValue is a
class with private state and keeps only its size assert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:32:19 +02:00
Niko ce8944f853 test(recompiler): plan an invariant descriptor phi in the disk cache test
The round trip requires some fixture to plan a phi, and only one did:
the loop counter of "bindless sample in a loop latch", which reaches the
plan through the latch condition. The counter is not invariant, so the
CPU walk can never evaluate that condition. A plan that keeps only the
conditions the walk can evaluate, as #5181 aims to, would hold no phi
and fail the check with "no fixture plans a phi".

"descriptor invariant phi" loads the image descriptor from the same SRT
offset on both sides of a branch on an SRT dword. Its eight dwords are
phis of equivalent reads: the tracker keeps them, since it splits only
descriptor phis that are not invariant, and the walk evaluates them
through the invariant read. The plan stores them as descriptor dwords,
whichever conditions it keeps, so the phi encoding stays covered. The
fixture goes after the other compute fixtures, so they keep their seeds
and their damaged-entry checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:31:17 +02:00
Niko 70663fec42 test(recompiler): check that APS5_NO_PERF_DISK_PLANS=1 is cache-neutral
APS5_NO_PERF_DISK_PLANS=1 is listed as a neutral switch, so it stays out
of the variant key, and it exists to compare warm starts with and
without stored plans. Nothing tested either property: the cross-process
test ran with the switch unset only.

A fourth child process now sets the switch against the cache the first
process filled. It must run the front end for all 14 fixtures, neither
load nor store a plan, still hit all 14 stored variants, and produce the
same SPIR-V, bindings, push constants, snapshots, read traces and plans
as the first process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:31:17 +02:00
Niko a69f5e0371 test(recompiler): compare stored resource plans field by field
The plan tests compared a decoded plan with the original only through
the codec itself (re-encoding must give the same bytes) and through
materialization. Neither catches a field that EncodePlan and DecodePlan
both leave out: the bytes still agree, and the walk does not read every
field. Leaving userDataBase, the plan's sRGB formats, the value ids or
several MemoryInfo fields (secondaryOffset, dataDwords, dataFormat,
planningOnly, idxen) out of both sides passed every test. The hand-built
plan also never materializes (its uniform values cannot be evaluated),
so its 1000 runtimes all failed with the same error, and no plan held an
immediate wider than 32 bits.

samePlan compares every field of two plans without the codec: the value
graph by storage index (opcode, type, id, flags, register, immediate,
arguments, phi blocks), the block ids, the roots, and the plain data
through the structures' own equality. The round trip now requires it
for the 14 fixtures and the hand-built plan, which sets every MemoryInfo
field, all indirect table fields, a second buffer, the whole uniform
fill and a 64-bit immediate to values other than their defaults. The
second process also builds each fixture's plan with its own front end
and requires it to equal the plan it used, so a stored plan must match
what the front end builds in another process, not only its own
re-encoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:31:17 +02:00
Niko ed3f2776ed fix(recompiler): reject a stored plan whose image table names a missing source
DecodePlan checked every descriptor source index a plan holds except the
two inside an indirect image table: the heap source and the material
source. A damaged entry could therefore decode with a table that points
past the plan's descriptor sources, and only the materializer's own
bounds checks stood between it and an out-of-range read. The front end
interns both sources before the image source that names them, so a plan
it builds always passes the new check.

agc_shader_disk_cache now requires a plan whose table heap or material
source lies past the sources not to decode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:31:17 +02:00
Niko 1ed6c21067 perf(recompiler): load the resource plan from the disk cache before the front end
getSource built ResourceProgram (PrepareResourceProgram and ExtractPlan)
for the first request of every shader a process saw, before
findOrCompileVariant looked in the disk cache. With a warm cache every
launch still decoded, built the CFG, structurized, translated and ran
SSA, the fold passes, SrtWalker and ResourceTracker for every shader;
only SPIR-V emission was skipped.

getSource now builds the plan key and tries LoadPlan first. On a hit it
sets the plan and leaves the program null: findOrCompileVariant already
rebuilds the program when no stored variant fits the layout. The loaded
plan takes the request's code address as its shader hash, as a plan
built in this process would. On a miss it builds the plan as before and
stores it. A plan the front end refuses is not stored. The disk is not
used when the cache is off, under the debug probe, with APS5_DUMP_IR
(which prints the front-end passes), or with APS5_NO_PERF_DISK_PLANS=1,
a cache-neutral switch for comparisons.

FrontEndBuilds() counts PrepareResourceProgram calls. The new
cross-process test prepares, captures and materializes 14 shader
fixtures in one process and repeats it in a second process sharing the
cache directory: the second process must run the front end 0 times, hit
14 plans and 14 variants, and produce byte-identical SPIR-V, bindings,
push constants, snapshots, read traces and plans. A damaged plan entry
is rejected and rebuilt alone with the same output and stored again; a
changed code word and another switch value miss.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:31:17 +02:00
Niko 0c144502e5 feat(recompiler): store front-end resource plans in the shader disk cache
The disk cache keeps compiled variants (SPIR-V, shader info, binding
layout) but not the IrResourcePlan the front end extracts for the
resource walk, so a process can only get a plan by running the whole
front end. Since #471 the plan owns its values (ownPlanValues clones the
graph behind every root into valueStorage and blockStorage), so it can
be written out and rebuilt.

- EncodePlan writes the value graph in storage order, each value as
  LEB128 integers: opcode, type, id, flags, register, operand count,
  the immediate if it has one, each argument as the zigzag distance to
  the value's own index (0 for null) and, for phis, the index of each
  predecessor block. The roots (descriptor source dwords, SRT reads,
  control-flow conditions, uniform-fill values) are value indices; the
  plain data (memory info, indirect image tables, control-flow edges,
  materialization sources, guarded, clean and pure flat slots, the
  flags, the ShaderInfo through the variant's encoder) follows. A plan
  whose roots or arguments point outside its storage is not encoded.
  Control-flow conditions can pull most of a program into the plan:
  a synthetic 2,038-dword compute shader with a branch every 20 dwords
  has 42,775 plan values, which take 518 KB this way and 2.5 MB with
  fixed-width fields; its compiled variant entry is 935 KB.
- DecodePlan rebuilds the graph the way ownPlanValues does and rejects
  an unknown opcode, an overlong integer, an index past the values or
  blocks, a descriptor source, image, buffer or sampler past the
  sources, a branch without two successors, an edge past the blocks,
  and a uniform fill of more than four words.
- BuildPlanKey is the in-memory source key (RecompileCacheKey::Build and
  the host subgroup size), every code word and the switch key, after a
  plan entry tag. It leaves out the binding layout, so all layouts of a
  shader share one plan.
- EncodePlanEntry/DecodePlanEntry use the variant's header and hashes;
  LoadPlan and StorePlan use the same directory and background writer
  and count plan hits, misses, writes and load failures on their own.
- Size asserts cover the plan and the structures it is written from.
- FormatVersion 30.

agc_shader_disk_cache now round-trips the plans of 14 in-tree shader
fixtures (SRT reads, guarded and chained pointers, control-flow
conditions, an indirect image table, a loop phi, a fragment shader) and
a hand-built plan with a uniform fill. For each it materializes the
original and the decoded plan over 1000 random user-data and memory
runtimes and requires the same snapshot, poison, null-root count and
read trace, or the same error. Truncated, bit-flipped and
other-format entries are rejected, malformed plans do not decode, and
the key follows the code, the target and the host subgroup size but not
the layout, addresses or user data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:31:17 +02:00
Hussein Kadhim 37ea3b651a fix(libc): finish the exit handlers when one of them calls exit
A guest handler that called exit threw out of the libc exit path because the exiting thread waited on itself. Like FreeBSD, the nested call now runs the handlers that are still registered and ends the process with the status it passed.
2026-10-13 16:28:00 +00:00
Niko 5c802bbf10 refactor(recompiler): split the ShaderInfo encoder and the entry envelope
encodeInfo and decodeInfo wrote the ShaderInfo fields inline between the
compiled-shader header and the binding layout, and EncodeEntry and
DecodeEntry built and checked the file header (magic, format, source
version, key and payload hashes) inline. Move both into helpers,
encodeShaderInfo/decodeShaderInfo and wrapEntry/unwrapEntry, so that a
second kind of entry can reuse them.

The bytes of a compiled variant entry do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:23:35 +02:00
Hussein Kadhim ac82f3c6a9 fix(libkernel): switch the host stack bounds in siglongjmp on Windows
longjmp moves the Windows stack bounds onto the stack of the target jump buffer so that exceptions and stack probes keep working after a jump between guest stacks. siglongjmp restored the same registers without that step, so a jump from a context stack back to the thread stack left the thread bounds describing the stack it had left.
2026-10-13 16:09:27 +00:00
hamb3r 8652dbe35b feat(libc): add missing Unity IL2CPP runtime imports and Dinkumware helpers
Add missing libc.prx symbols required by Unity IL2CPP runtime:
- Implement _Dtest double classification in Math.cpp
- Implement _Inf double constant in Math.cpp
- Implement sceLibcBacktraceGetBufferSize and sceLibcBacktraceSelf in Backtrace.cpp
- Implement _Cnd_signal in Threads.cpp
- Implement _ZNSt8ios_base7_AddstdEPS_ in RuntimeSupport.cpp
- Implement __pointer_type_info and __function_type_info vtables in TypeInfo.cpp
- Remove duplicate stubs from libSceAmpr/Export.cpp
- Add unit tests in GuestMath.cpp and GuestLibcThreads.cpp
- Document unverified behaviors in docs/dev/debt/libc.md
2026-10-13 19:01:30 +03:00
Adrià 0214069e33
Merge pull request #5263 from adriadam10/ci/mesa-cache-per-cpu
ci(build): key the Mesa shader cache by the runner's CPU flags
2026-10-13 17:58:02 +02:00
Adrià Franch 32accfa938 ci(build): key the Mesa shader cache by the runner's CPU flags
The cache from #5107 was keyed by Mesa version only, so a runner restored
llvmpipe code compiled on another runner's CPU and about 85 agc tests died
with SIGILL (Illegal instruction) on main.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 17:57:49 +02:00
NaitikJoshiPro 6f856a7cda feat(relinker): executable TLS relocations (TPOFF64, DTPOFF64, DTPMOD64)
A PS5 eboot can keep initial-exec TLS as R_X86_64_TPOFF64 GOT relocations
(SuperTuxKart, STKT00001, has 13, all without a symbol), which the
relocation allowlist rejected. Relocations against the executable's own
TLS block (no symbol, or a defined STT_TLS symbol folded into the addend)
now go to the Linux ELF as symbolless dynamic relocations that the
dynamic loader resolves against where it placed the block; one against an
import stays named and is typed STT_TLS. The Windows and macOS images
resolve TPOFF64 and DTPOFF64 statically against the block
AlignUp(p_memsz, p_align) below the thread pointer, the offset
WindowsTlsBuilder and MacOsImage use, and reject DTPMOD64 and imported
TLS. TPOFF32 stays unsupported.

New test executable_tls_relocations covers the Linux, Windows and macOS
outputs and the rejected cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:24:37 +05:30
Adrià 9d0a41d5a1
Merge pull request #4611 from EagleBearerYS/fix/libscenet-epoll-out
fix(libSceNet): sceNetEpollWait takes 2 as the OUT event
2026-10-13 17:51:30 +02:00
Adrià 34cee1ef15
Merge pull request #4608 from MiniRML/fix/relinker-icon-png-chunks
fix(relinker): validate icon PNG critical chunks and order
2026-10-13 17:50:39 +02:00
Adrià 3e8d0793b7
Merge pull request #4588 from Nixitov/feat/libkernel-host-hardware
feat(libkernel): report the host SoC and CPU temperature
2026-10-13 17:49:54 +02:00
MarcZX100 85c090d49d test(libkernel): verify sigwait preserves errno 2026-10-13 15:49:45 +00:00
Hussein Kadhim 13b4de565b fix(libkernel): return the error number from sigwait
FreeBSD's sigwait hands the error number back as its return value and leaves errno alone; the guest export returned -1 and set errno.

(cherry picked from commit b627f6c9443dd94a6392959f33e52d837ee4cae4)
2026-10-13 15:49:28 +00:00
Adrià 27477e5f47
Merge pull request #4583 from MiniRML/fix/relinker-section-name-address
fix(relinker): zero the unmapped section-name table address
2026-10-13 17:49:15 +02:00
Adrià 8b20708599
Merge pull request #4579 from MiniRML/fix/release-configured-windows-runtime
fix(release): find the configured Windows runtime
2026-10-13 17:48:48 +02:00
Adrià 9dd3c2d1a6
Merge pull request #4547 from codingreapermaps/feat/relinker-trace-imports
feat(relinker): add --trace-imports for Windows executables
2026-10-13 17:48:21 +02:00
Adrià 67e86d93d5
Merge pull request #4250 from shahulmubeen-a/perf/shader-byte-buffer-stores
perf(shader): store 8- and 16-bit buffer components as bytes
2026-10-13 17:47:54 +02:00
Adrià c7beafd21a
Merge pull request #5256 from adriadam10/fix/libc-acosf-nan-bits
fix(libc): choose the acosf NaN bits instead of the host libm
2026-10-13 17:46:36 +02:00
Adrià Franch 0265f33636 fix(libc): choose the acosf NaN bits instead of the host libm
FreeBSD msun e_acosf.c returns (x-x)/(x-x) for |x| > 1 and NaN inputs: the
x86 default NaN 0xffc00000, or the quieted input NaN. std::acos left it to the
host libm, and guest_math failed on the runner's glibc 2.39.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 17:46:18 +02:00
Adrià 091c136ba6
Merge pull request #5223 from Nixitov/feat/libc-float-math
feat(libc): implement the missing float rounding and classification exports
2026-10-13 17:43:06 +02:00
Adrià 6679168360
Merge pull request #5210 from ErenYeager1836/carry/libkernel-coredump-user-file
feat(libkernel): answer the coredump user-file attach calls
2026-10-13 17:42:50 +02:00
Adrià 5030fbb5cb
Merge pull request #5201 from ErenYeager1836/carry/libkernel_sys-chroot
feat(libkernel_sys): implement chroot with FreeBSD lookup semantics
2026-10-13 17:42:22 +02:00
Niko 0f7cd5b7ac fix(conventions): skip library test fixtures in the export checks
The duplicate-export and silent-stub rules treated every APS5_VABI
definition under core/libs/prx/ as a library export, including mocks
and callbacks in a library's tests/ directory. Those files are built
only into separate test executables, never into the library, so they
are not exports. PR #3533 failed CI because a sceKernelGetProcessTime
mock in libSceAudioOut/tests was reported as a duplicate of libkernel's
export. An sce* fixture with a stub-shaped body was also reported as a
silent stub, and a real export that shares a name with a fixture in
another library's tests/ was reported as a duplicate.

Skip files under a tests directory on both sides of the check, with the
same test code() already uses for title-specific and progress.py uses
for its function counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:39:20 +02:00
Adrià fdefd76609
Merge pull request #4705 from karanvora2599/perf/shader-unary-input-flush-once
perf(shader): drop the input flush that rcp, rsq, sqrt, exp and log results never use
2026-10-13 17:38:07 +02:00
Adrià e6fae88a42
Merge pull request #4684 from XairRrR/fix/agc-acquire-without-waiting
fix(agc): drop a presentation instead of waiting for a swapchain image
2026-10-13 17:36:29 +02:00
Adrià 068ab2aba6
Merge pull request #4638 from MarcZX100/codex/carry-win-getdents-throw-3327
fix(libkernel): throw for oversized directory entries
2026-10-13 17:32:31 +02:00
Adrià 6629c09392
Merge pull request #4619 from MiniRML/fix/nid-version-requirement-chains
fix(nid): bound version requirement chains to their section
2026-10-13 17:31:25 +02:00
Adrià 3f2782cc7c
Merge pull request #4618 from Borooooo/fix/relinker-long-paths
fix(relinker): accept paths longer than 260 characters on Windows
2026-10-13 17:31:05 +02:00
Adrià 765661581b
Merge pull request #4613 from EagleBearerYS/fix/libscenet-copied-address-length
fix(libSceNet): report copied socket address lengths
2026-10-13 17:30:34 +02:00
Adrià c35bc4b92c
Merge pull request #4612 from EagleBearerYS/fix/libscenet-epoll-ident
fix(libSceNet): report the registered socket as the epoll event ident
2026-10-13 17:30:05 +02:00
Adrià 2f6ae9fc88
Merge pull request #4610 from EagleBearerYS/fix/libscenet-sce-socket-timeouts
fix(libSceNet): take 0x1105 and 0x1106 as the send and receive timeouts
2026-10-13 17:28:11 +02:00
Adrià 842766d39c
Merge pull request #4606 from Nixitov/feat/import-audit-stop
feat(tools): report each converted title's current stop in import_audit
2026-10-13 17:26:59 +02:00
Adrià 9c4686321b
Merge pull request #4604 from MiniRML/fix/nid-gnu-hash-geometry
fix(nid): validate GNU hash geometry before rewriting
2026-10-13 17:26:25 +02:00
Adrià 284cb63dc9
Merge pull request #4601 from MiniRML/fix/nid-pe-export-references
fix(nid): resolve PE reference exports through their ordinals
2026-10-13 17:26:06 +02:00
Adrià 39890031e3
Merge pull request #4591 from MiniRML/fix/oracle-input-rows
fix(oracle): validate input words before tool lookup
2026-10-13 17:25:30 +02:00
Adrià a56c905df6
Merge pull request #4589 from MiniRML/fix/tools-cut-helper-accounting
fix(tools): exclude cut helpers from NID accounting
2026-10-13 17:24:55 +02:00
Adrià 9c05bf25f5
Merge pull request #4585 from jagrat04/feat/libkernel-cpuset-affinity-errors-pr
feat(libkernel): return ESRCH and EINVAL for unsupported cpuset affinity targets
2026-10-13 17:23:01 +02:00
Adrià 5a76fd716f
Merge pull request #4575 from MiniRML/fix/progress-raw-string-literals
fix(progress): ignore C++ raw-string contents
2026-10-13 17:19:23 +02:00
Adrià 382c451acf
Merge pull request #4564 from MiniRML/fix/nid-windows-unicode-paths
fix(nid): preserve Unicode paths on Windows
2026-10-13 17:19:00 +02:00
Adrià c422b57fb3
Merge pull request #4672 from milescoviello/carry/lseek-pipe-windows-4670
fix(libkernel): fail lseek on a pipe with ESPIPE on Windows
2026-10-13 17:13:35 +02:00
Adrià 7e4edf509a
Merge pull request #4616 from EagleBearerYS/fix/libc-strtod-quiet-nan-windows
fix(libc): return the default quiet NaN from the strtod family
2026-10-13 17:11:55 +02:00
billybobpicklio d7818c9e55 fix(agc): accept integer tessellation partitioning
A draw with VGT_TF_PARAM.PARTITIONING 0 (PART_INTEGER) was refused by the
state decoder and its hull and domain programs by the recompiler. It is now
drawn with Vulkan's equal spacing, the mode radeonsi programs as
PART_INTEGER, with tessellation factors bounded at 64 instead of
fractional-odd's 63. Power-of-two and fractional-even partitioning still
throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:11:46 -04:00
Adrià cba01c87d1
Merge pull request #4614 from EagleBearerYS/fix/libscenet-getsockname-unbound-windows
fix(libSceNet): name an unbound socket with the zero address on Windows
2026-10-13 17:11:20 +02:00
Nixitov 00f0589101 feat(libc): implement the float rounding and classification exports
PPSA99611 imports ceilf, floorf, truncf, isnan and isinf from libc.prx and stops at start-up on main. Implement them next to the existing double forms: the rounding results delegate to the standard library like the other float exports, and isnan/isinf reuse the __isnan/__isinf helpers isnanf already uses.
2026-10-13 17:09:47 +02:00
Adrià 242bae7445
Merge pull request #4597 from MiniRML/fix/release-destination-preflight
fix(release): preflight asset destination types
2026-10-13 17:07:51 +02:00
Adrià 9510e71d6a
Merge pull request #5221 from adriadam10/ci/dispatch-without-linux
ci(build): let a dispatched build skip the Linux job
2026-10-13 17:05:09 +02:00
Adrià eb5bee4bfe
Merge pull request #4557 from saxopa/fix/relinker-linux-program-header-slots
fix(relinker): move the Linux program header table when it has no free slot
2026-10-13 17:04:41 +02:00
Adrià 8bf10e51f4
Merge pull request #4554 from queeeef/fix/agc-sampled-dcc-key-writers
fix(agc): wait for recorded writes over a sampled texture's DCC keys before reading them
2026-10-13 17:04:12 +02:00
Adrià Franch 71f98dd746 ci(build): let a dispatched build skip the Linux job
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 17:03:50 +02:00
Adrià d67a9e5a45
Merge pull request #4553 from Nixitov/feat/fchmodat
feat(libkernel_sys): implement fchmodat
2026-10-13 17:03:44 +02:00
Adrià 1a217cd4a3
Merge pull request #5204 from ErenYeager1836/carry/appcontent-addcont-enqueue-download
feat(libSceAppContent): implement sceAppContentAddcontEnqueueDownload
2026-10-13 17:02:04 +02:00
Adrià a74f2efe84
Merge pull request #5097 from TideOPL/fix/ancillary-layer-export
fix(shader): read the ancillary render target index from Layer when the draw exports it
2026-10-13 17:01:11 +02:00
Adrià Franch 1a2a4265c1 docs(debt): record the GS_FAST_LAUNCH decode assumptions 2026-10-13 17:00:51 +02:00
Lexroid 7370aaf6d0 feat(agc): decode GS_FAST_LAUNCH geometry with one point per subgroup
VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1 (mesh-style NGG, as RADV programs
it on gfx10.3) was refused. With a point list and GE_CNTL groups of one
point, it launches one subgroup per input point and the first thread
reads that point's index in the ES vertex id VGPR, which the mesh
prologue already provides. The subgroup's primitive limit comes from
GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR, and the GS and ES VGPR counts are
not required to be full.

Fast launch 2, other group sizes or inputs, and indexed fast-launch
draws still throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:00:45 +02:00
adriadam10 bc568a1f6f fix(input): MouseModeEnabled reads the keyboard/mouse state
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 17:00:28 +02:00
AlxcBTW 38bc0fd348 feat(input): turn mouse speed into a smooth right-stick tilt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:00:20 +02:00
Adrià 3b94f923a5
Merge pull request #4633 from QRMOE/fix-libc-wcsxfrm-truncation
fix(libc): terminate truncated wcsxfrm results
2026-10-13 17:00:13 +02:00
Adrià fffe7351ad
Merge pull request #4625 from QRMOE/fix-libc-strtok-exhausted-state
fix(libc): clear exhausted strtok continuation state
2026-10-13 16:59:43 +02:00
Adrià 72a8e67122
Merge pull request #4621 from MiniRML/fix/nid-elf-shared-string-roles
fix(nid): preserve each ELF string consumer independently
2026-10-13 16:59:13 +02:00
codingreapermaps 430e5c833e feat(relinker): add --trace-imports for Windows executables
With --trace-imports the entry stub resolves TraceImport_nid_no_patch
in libc.prx and passes each function import through it before writing
the slot: every PLT slot, and a GOT slot only when the original symbol
is a function, so data imports such as __stdoutp keep their address.
Without the option the output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:55:31 +02:00
Dean Galvin 0adbdcf45c perf(agc): cache a draw key the second time it is seen
A draw whose key has no entry captured its stages, built a variant per
stage (a copy of every captured region) and inserted a draw entry. A key
that never comes back paid for the variants and the entry for nothing,
and its entry pushed a live one out of the cache. Draws whose user data
changes every frame are such keys.

The driver now remembers the keys it has seen once, in a table of
four-way buckets holding four times the cache's size in keys. A key gets
its entry at its second sight; a key that already has an entry still
takes new variants at once. A full bucket replaces its oldest key, so
keys that share a bucket do not push each other out until more than four
of them come between two sights of one.

Test: agc_driver_draw_cache_reuse draws three times. The first two draws
find no entry, one entry is inserted, and the third draw hits it. A
sighting table of one bucket keeps two keys drawn in turn and replaces
its oldest key when full.
2026-10-14 03:53:28 +13:00
Adrià 60fb618708
Merge pull request #4551 from eljackjoestar-cmd/codex/preserve-color-slice-xor
fix(agc): preserve array slice XOR in color target views
2026-10-13 16:52:35 +02:00
Adrià c496fd530f
Merge pull request #4546 from codingreapermaps/feat/libc-call-trace-runtime
feat(libc): record traced import calls on Windows
2026-10-13 16:51:55 +02:00
Nixitov a3a3f3a9ce feat(tools): report each converted title's current stop in import_audit
The stop is the first unresolved import in the registry order, the line the loader fails on. Print it per registry and add it to the --json result.
2026-10-13 16:50:51 +02:00
Adrià b8683834f3
Merge pull request #4596 from MiniRML/fix/oracle-target-override
fix(oracle): honor the current target override
2026-10-13 16:48:06 +02:00
Adrià f2c314e902
Merge pull request #4577 from Moaziz667/fix/prime-remaining-host-mutexes
fix(agc,shader): prime the remaining host shared mutexes
2026-10-13 16:45:17 +02:00
Rick 8ea8ea386a fix(libSceKeyboard): report one key slot when no key is held
A keyboard record on the console never counts fewer than one key: with
nothing held, and with no keyboard connected, the count is 1 and that
slot holds code 0. SharpProspero's keyboard reader, PS5_Quake3e's notes
on records read from a console and the PS5 input research notes all say
so. AnyPS5 reported a count of 0. The count is now at least 1; the key
slots themselves are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 16:44:17 +02:00
Adrià 2f5c3a4bf5
Merge pull request #4559 from MarcZX100/codex/import-audit-sectionless-modules
fix(tools): audit sectionless guest modules
2026-10-13 16:43:53 +02:00
Adrià b4d76ba609
Merge pull request #4556 from Amoenus/codex/fix-inactive-covered-centroid
fix(agc): admit inactive covered-centroid mode
2026-10-13 16:43:05 +02:00
screamm 204355cc64
feat(libkernel): answer the coredump user-file attach calls
sceCoredumpAttachUserFile (5nc2gdLNsok) and
sceCoredumpAttachMemoryRegionAsUserFile (MEJ7tc7ThwM) get the
signatures of Orbital's db_types.json (cc408367) and SharpProspero's
KernelSystem.cs (5dfdb4b0): (uint32_t userValue, const char* path) and
(uint32_t userValue, const void* mem, size_t size, const char* name).

The handler is never invoked here, so both return
SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER (0x81180003, shadPS4
coredump_error.h e0a46675) and attach nothing; a null path or name and
a null, empty or wrapping region throw because the order of the
console's checks is unknown.

Carried from @screamm with author attribution preserved.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 20:11:39 +05:30
Adrià ffa2a6ecd3
Merge pull request #4550 from MiniRML/fix/nid-buffer-validation
fix(nid): validate buffer ranges and string terminators
2026-10-13 16:41:04 +02:00
SandstromPL 78f620cf9e fix(relinker): keep code analysis inside the code extent of PS4 executables
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:51 +05:30
Adrià 69b4ca5d9b
Merge pull request #4215 from Mouad-Scriptz-0/fix/agc-flip-eop-event
fix(libSceAgcDriver): raise the graphics end-of-pipe event from flip packets
2026-10-13 16:39:04 +02:00
MiniRML bfffd3359c fix(relinker): validate icon PNG critical chunks and order 2026-10-13 16:38:28 +02:00
Nesyth 417ea99df5 fix(libkernel): zero-fill APR reads that reach the end of the file 2026-10-13 16:38:17 +02:00
Zenodeon b6c224af45 fix(relinker): keep diagnostics compatible with pending refactors 2026-10-13 18:37:50 +04:00
Zenodeon 36c11dc926 fix(relinker): identify failed inputs and invalid dynamic values 2026-10-13 18:37:50 +04:00
JJenkx f7fd7569fa fix(agc): retire fully unmapped multisample targets 2026-10-13 10:37:07 -04:00
Shahul Mubeen A 066a8fbe30 perf(shader): store 8- and 16-bit buffer components as bytes in compute
A formatted store of an 8- or 16-bit component into a bound buffer
patched the dword around it with a compare-exchange loop (writeBuffer
in SpirvRuntimeBuffer.cpp). Ghost of Yotei's light-culling compute
pass (program 0x800038be00, 240x135 groups per frame) stores millions
of such components per dispatch and took 426 ms of GPU time per
dispatch on an RTX 4070 Ti SUPER.

Compute stores now go through a byte view of the bound buffers
(buffers_u8, an aliased declaration of the buffers binding, defined
on the first narrow store): one OpStore per byte under
StorageBuffer8BitAccess, which the shader device profile already
requires. Without the capability, and in every other stage, the
compare-exchange path remains: the draw-time validator accepts only
u32 and u64 aliased views. Loads, dword stores and stores through
GPU-selected descriptors (BDA) are unchanged.

Ghost of Yotei (PPSA26344 02.002.000), Windows 11, RTX 4070 Ti SUPER,
logo phase: that dispatch 426 -> 10.7 ms of GPU time, 1.9 -> 11 fps.

agc_formatted_buffer_byte_stores compiles a formatted 8_8_8_8 store
as compute with and without StorageBuffer8BitAccess and as a vertex
shader with it, and checks the byte view, the byte stores, the
compare-exchange loops and the fallbacks. agc_driver_formatted_store_unaligned
executes 8- and 16-bit formatted stores at misaligned offsets,
including stores that cross a dword, and checks every byte of the
output including the untouched neighbours.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 16:36:56 +02:00
Adrià e0d388fa28
Merge pull request #4555 from Xabilimon1/fix/macos-build-libkernel-getrlimit
fix(libkernel): build the fcntl status flags and the getrlimit test on macOS
2026-10-13 16:36:38 +02:00
Adrià Franch c6b1f84a7e docs(debt): record sceAppContentAddcontEnqueueDownload behaviour as unverified 2026-10-13 16:33:51 +02:00
David fdfe2204aa feat(libSceAppContent): implement sceAppContentAddcontEnqueueDownload
The signature (uint32_t service label, const NpUnifiedEntitlementLabel*)
follows SharpProspero Interop/AppContent/AppContent.cs (5dfdb4b0). dlc_emu
dlc_content.cpp (fcf69b16) answers NOT_FOUND for a label that is not a
known add-on and PARAMETER for a null label. No add-on content is ever
installed here, as sceAppContentAddcontMount and sceAppContentGetAddcontInfo
already model, so every non-null label answers NOT_FOUND and nothing is
queued. dlc_emu's nonzero-padding check is left out, as sibling functions
do not check it and no console source confirms it.

Carried from @screamm with author attribution preserved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:33:45 +02:00
Adrià b478fbbb1c
Merge pull request #5191 from p3n1cillin/claude/carry-chroot
feat(libkernel_sys): add chroot that keeps the guest root
2026-10-13 16:33:00 +02:00
Adrià 9dac817b19
Merge pull request #4548 from coltken95/fix/libc-printf-nan
fix(libc): print NaN without a sign in the guest printf formatter
2026-10-13 16:31:34 +02:00
Adrià c02df171c8
Merge pull request #4545 from thuanquang/test/tools-python39-hard-links
test(tools): create hard links with os.link for Python 3.9
2026-10-13 16:30:44 +02:00
Adrià 2672a73178
Merge pull request #4501 from devavijitsingh/fix/diagnose-self-module-skip
fix(relinker): warn when SELF modules are skipped
2026-10-13 16:30:12 +02:00
Nixitov 0f73f5f057 feat(libkernel): report the host SoC and CPU temperature
PPSA99058 and PPSA99420 import sceKernelGetCpuTemperature and sceKernelGetSocSensorTemperature and stop at start-up on main. Read the host CPU/package, board and NVMe sensors (hwmon/thermal on Linux, the ACPI thermal zones and the storage temperature property on Windows) and report an unsupported sensor with EINVAL.
2026-10-13 16:30:07 +02:00
Adrià 80e70b4292
Merge pull request #4408 from santhoshkumar-dev/codex/agc-render-target-vertex-aliasing
fix(agc): exclude skipped vertex records from target alias checks
2026-10-13 16:29:52 +02:00
Adrià b0d41be1ff
Merge pull request #4283 from MarcZX100/codex/carry-guest-thread-stacks-3283
fix(libkernel): place Linux guest thread stacks inside application area
2026-10-13 16:28:18 +02:00
MiniRML a8a78ded96 fix(progress): load the tokenizer beside the source file 2026-10-13 16:27:51 +02:00
MiniRML f37eecfdac fix(progress): ignore C++ raw-string contents 2026-10-13 16:27:51 +02:00
Adrià ef42c5f2af
Merge pull request #5183 from p3n1cillin/claude/carry-clock-settime
feat(libkernel): add clock_settime with FreeBSD argument checks
2026-10-13 16:27:05 +02:00
eljackjoestar-cmd 13818efc3b fix(agc): preserve array slice XOR in color target views 2026-10-13 16:25:05 +02:00
screamm f8a269e80a
feat(libkernel_sys): let chroot keep the guest root
Porpoise (PPSA99764) stops on its weak chroot import (#3632, #4497).
Run 5 on main c2565f014, and again on a copy of that runtime:

  FAIL: unresolved ELF import AKknh8Cx-V8 from libkernel_sys.prx

Its only call is in put_root_back (src/porpoise_jailbreak.cpp,
elripalda/Porpoise-Dolphin-Emulator-for-PS5 2ed1ab5a06995aa1): after a
jailbreak daemon has made the console's root its root and /app0 is
gone, it calls fchdir on the descriptor of "/" it opened at start, then
chroot("."), and checks the result. So it only ever puts back the root
it started in, and it does not use paths under any other root.

On top of the lookup errors of the previous commit, chroot now follows
FreeBSD 11.4 sys_chroot and pwd_chroot (8c639945, vfs_syscalls.c and
kern_descrip.c) for that case: a directory that is the guest root
("/", "/..", "." after fchdir to "/") succeeds without a change, since
fd_rdir would be set to the vnode it already is. The guest is taken
never to have changed its root, so with chroot_allow_open_directories 1
an open directory does not give EPERM. ENAMETOOLONG is added as in
pathconf, and a ".." through a file reports ENOTDIR instead of
escaping as a filesystem_error. Any other directory still throws: the
resolver's root and mount aliases are fixed, and no title is known that
needs a real new root.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 19:54:29 +05:30
screamm 7e5bec09f8
feat(libkernel_sys): add chroot with FreeBSD lookup errors
Porpoise (PPSA99764) imports chroot (AKknh8Cx-V8) from
libkernel_sys.prx, not libkernel.prx, one reference. Predicted stop
once pathconf, the pthread_attr calls and ftrylockfile ahead of it are
covered (not a run):

  FAIL: unresolved ELF import AKknh8Cx-V8 from libkernel_sys.prx

The NID is computed with tools/nid_names.py and matches aerolib.

Follows FreeBSD 11.4 sys/kern/vfs_syscalls.c sys_chroot and change_dir
(8c639945a44588f6cfdddfb057bb5aad88735da0) for a root caller up to the
lookup: EFAULT for a null path, ENOENT for an empty or missing one,
ENOTDIR for a file or a path through a file. A directory that is found
throws with the path named: the resolver's root is fixed and the mount
aliases (/app0, /savedata0) are host paths that do not move with it,
so it cannot be re-based, and a real chroot is not modelled. Nothing
shows that Porpoise calls it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 19:54:29 +05:30
TideOPL de5279bc0c fix(shader): serialize the pixel layer export flag from request version 23
main allocates request version 22 to the tessellation patches per group, so a version 22 request has no layer export byte.
2026-10-13 15:23:21 +01:00
Adrià 146f171549
Merge pull request #4271 from eljackjoestar-cmd/codex/reclaim-source-ir
perf(shader): reclaim unreachable cached source IR values
2026-10-13 16:22:26 +02:00
saxopa b3fa9d8318 fix(relinker): move the Linux program header table when it has no free slot
Linux output reused the input program header table in place, so it could
hold no more entries than the input declared. A title whose kept segments
and the five synthetic headers did not fit lost PT_GNU_EH_FRAME with a
warning, or failed with "Not enough program header slots". When more
headers are needed, the table is now written at the end of the extra block,
e_phoff points there and PT_PHDR maps it through the extra block PT_LOAD;
the header block then maps only the ELF header. Bytes after the input table
are left as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:20:00 +02:00
Daan Dehing a910dee09f fix(agc): wait for recorded writes over a sampled texture's DCC keys before reading them
textureClearKeys reads the keys through a raw pointer, past the flush hook, so writes the
recorder holds for the key range are not in the bytes yet. A title that clears the keys of a
target with a kernel and samples it in the same batch had its keys read from the memory's
previous contents: in Palworld (PPSA23126) these were the float data of a reused heap, so the
keys read as mixed and the texels were read as stored.

When the keys read do not decode (not uncompressed, not a clear the format can fill),
WaitForKeyWriters answers from the pending fills and stores it knows, or waits for the batch
that writes the keys, and the keys are read again. Keys that decode are used as before.

RunSampledKeysBehindKernel (agc_driver_dcc_keys_first_write): key memory holding float bytes,
then the 0000 clear kernel recorded without a wait; the sampled texture's keys read Clear0000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:18:50 +02:00
Adrià 759c66564f
Merge pull request #4428 from hypermoist/perf/address-space-import-runs
perf(agc): keep adjacent ranges one import serves as one region of the cached address space
2026-10-13 16:18:14 +02:00
Alexej Szpevák fc8165e8b9
test(cli): cover module-dir, sce-module-path and target option errors
Add parser tests for --module-dir, --sce-module-path, --macos and
--to-rosetta, including their repeat and conflict errors, which had no
coverage.
2026-10-13 16:17:23 +02:00
TideOPL 7015c0443e fix(shader): read the ancillary render target index from Layer when the draw exports it 2026-10-13 15:15:55 +01:00
Adrià 5d7f1ffc1c
Merge pull request #4217 from Mouad-Scriptz-0/feat/videoout-mouse-capture
feat(libSceVideoOut): capture the host cursor for guest mice and scale their deltas
2026-10-13 16:15:45 +02:00
Adrià ac6d1b2013
Merge pull request #4163 from Celegans12/feat/agc-tessellation-patch-inputs
feat(agc): read hull patch constants in tessellation domain programs
2026-10-13 16:15:23 +02:00
Adrià Franch 603d94a53f test(libkernel_sys): reuse longName in the fchmodat check 2026-10-13 16:14:56 +02:00
Nixitov e3c1d01815 feat(libkernel_sys): implement fchmodat
2 Ship 2 Harkinian (PPSA99621) imports fchmodat through std::filesystem::permissions and stops at start-up on main. Resolve AT_FDCWD and absolute paths as usual and join other relative paths to the directory descriptor like openat, accept 0 and AT_SYMLINK_NOFOLLOW, and mask the mode to 07777.
2026-10-13 16:14:53 +02:00
codingreapermaps 3953a8d49e feat(libc): record traced import calls on Windows
TraceImport_nid_no_patch wraps a function of a library named in
APS5_TRACE_CALLS in a thunk that records the import, the thread and
the caller in a ring and counts the call, then jumps to the function.
One target keeps one thunk, so function pointers compare equal. The
crash and abort reports print the last 64 calls and the 32 most called
imports. Nothing calls TraceImport_nid_no_patch yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:13:12 +02:00
Adrià 8c772ad342
Merge pull request #4265 from fisherjoey/fix/fiber-host-stack
fix(libSceFiber): run fibers on host stacks with room for host calls
2026-10-13 16:12:14 +02:00
Adrià ab1755c258
Merge pull request #4459 from LeandroLP23/docs/contributing-gpus-tested
docs(contributing): ask GPU changes to name the GPUs their tests ran on
2026-10-13 16:11:54 +02:00
screamm 41b0ca597b feat(libkernel_sys): let chroot keep the guest root
Porpoise (PPSA99764) stops on its weak chroot import (#3632, #4497).
Run 5 on main c2565f014, and again on a copy of that runtime:

  FAIL: unresolved ELF import AKknh8Cx-V8 from libkernel_sys.prx

Its only call is in put_root_back (src/porpoise_jailbreak.cpp,
elripalda/Porpoise-Dolphin-Emulator-for-PS5 2ed1ab5a06995aa1): after a
jailbreak daemon has made the console's root its root and /app0 is
gone, it calls fchdir on the descriptor of "/" it opened at start, then
chroot("."), and checks the result. So it only ever puts back the root
it started in, and it does not use paths under any other root.

On top of the lookup errors of the previous commit, chroot now follows
FreeBSD 11.4 sys_chroot and pwd_chroot (8c639945, vfs_syscalls.c and
kern_descrip.c) for that case: a directory that is the guest root
("/", "/..", "." after fchdir to "/") succeeds without a change, since
fd_rdir would be set to the vnode it already is. The guest is taken
never to have changed its root, so with chroot_allow_open_directories 1
an open directory does not give EPERM. ENAMETOOLONG is added as in
pathconf, and a ".." through a file reports ENOTDIR instead of
escaping as a filesystem_error. Any other directory still throws: the
resolver's root and mount aliases are fixed, and no title is known that
needs a real new root.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 15:11:08 +01:00
screamm dc0d316fda feat(libkernel_sys): add chroot with FreeBSD lookup errors
Porpoise (PPSA99764) imports chroot (AKknh8Cx-V8) from
libkernel_sys.prx, not libkernel.prx, one reference. Predicted stop
once pathconf, the pthread_attr calls and ftrylockfile ahead of it are
covered (not a run):

  FAIL: unresolved ELF import AKknh8Cx-V8 from libkernel_sys.prx

The NID is computed with tools/nid_names.py and matches aerolib.

Follows FreeBSD 11.4 sys/kern/vfs_syscalls.c sys_chroot and change_dir
(8c639945a44588f6cfdddfb057bb5aad88735da0) for a root caller up to the
lookup: EFAULT for a null path, ENOENT for an empty or missing one,
ENOTDIR for a file or a path through a file. A directory that is found
throws with the path named: the resolver's root is fixed and the mount
aliases (/app0, /savedata0) are host paths that do not move with it,
so it cannot be re-based, and a real chroot is not modelled. Nothing
shows that Porpoise calls it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 15:11:08 +01:00
Yunus Shaikh 6a46563a93 refactor(savedata): clear the mount result once the mount point is made
#5173 changes the create condition right after the last error check, and #3950 removes the comment after the create block, so clearing the result in either place made the pull requests conflict. The result is still cleared only after every error return and before anything is written to it.
2026-10-13 07:10:41 -07:00
Adrià 91ed4ce625
Merge pull request #5174 from AkulTyagi11/feat/libc-implement-powidf2
feat(libc): implement __powidf2
2026-10-13 16:09:23 +02:00
Mouad-Scriptz-0 94087d08b0 feat(libSceVideoOut): capture the host cursor for guest mice and scale their deltas
The console's mouse reports relative motion, so the host cursor can never line
up with the cursor a title draws. While a title has a mouse open and the
window is focused the host cursor is hidden and captured, released on focus
loss or while the pad's mouse-to-stick toggle is on. Motion deltas are scaled
from the presented image's size in window units to the title's display
resolution with sub-pixel carry, so the title's cursor travels as far as the
host pointer would, on scaled desktops and letterboxed windows alike.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 14:05:52 +00:00
Tomás Ortellado f0da9edbfe fix(relinker): reject overflowing LSDA landing pad addresses 2026-10-13 11:05:22 -03:00
Adrià d0711248b1
Merge pull request #4279 from karanvora2599/fix/shader-coherent-gds
fix(shader): declare the GDS buffer coherent
2026-10-13 16:04:55 +02:00
Adrià Franch 745857448e test(libkernel): check the host-only stack guard on Linux only
Windows guest threads keep their normal stacks, so there is no host-only guard page below the guest stack to query.
2026-10-13 16:03:39 +02:00
MarcZX100 9b6e7f66c4 fix(libkernel): protect host-only guest stack pages 2026-10-13 16:03:36 +02:00
Cesar Rodriguez 7637cac13e fix(libkernel): place guest thread stacks inside the application area
On Linux, glibc maps the stack of every guest thread near the top of the
host address space (around 0x7fff00000000). On the console, application
stacks lie inside the application area below 0xFC00000000, and guest code
relies on that: ChakraCore's garbage collector indexes a 4 MiB table with
address >> 18, so it only covers addresses below 1 TiB and reports
Js::OutOfMemoryException for a stack above it.

Map each guest thread's stack with DoMapAnon, which allocates from the
guest arena, behind a PROT_NONE guard page, and pass it to pthread_attr_setstack.
The stack is freed after a join; the stack of a detached thread is freed once its
host thread is gone from /proc/self/task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:03:36 +02:00
Adrià e0263e2b87
Merge pull request #5065 from MarcZX100/codex/cxa-thread-atexit
fix(libc): export __cxa_thread_atexit
2026-10-13 16:03:19 +02:00
milescoviello 0d30059bf5 fix(libkernel): handle FIOCLEX and FIONCLEX in ioctl
ioctl() on a file, directory, pipe or character device threw "request
0x20006601 ... is not implemented" for FIOCLEX, and the same for
FIONCLEX, although the close-on-exec flag they change is already kept
for these descriptors and reported by fcntl(F_GETFD).

FreeBSD 11.4 handles both requests in kern_ioctl() for every descriptor,
before the request reaches the file's own ioctl, by changing the flag
F_GETFD reads. sys/kern/sys_generic.c, kern_ioctl():

	case FIONCLEX:
		fdp->fd_ofiles[fd].fde_flags &= ~UF_EXCLOSE;
		goto out;
	case FIOCLEX:
		fdp->fd_ofiles[fd].fde_flags |= UF_EXCLOSE;
		goto out;

sys/kern/kern_descrip.c, kern_fcntl():

	case F_GETFD:
	...
			td->td_retval[0] =
			    (fde->fde_flags & UF_EXCLOSE) ? FD_CLOEXEC : 0;

Both requests now set or clear that flag and return 0. libuv before 1.44
sets close-on-exec this way on FreeBSD (uv__cloexec_ioctl).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:59:42 +01:00
nabilfp ae9978a98e feat(libkernel): add clock_settime with FreeBSD argument checks
PS5MC (PPSA99420, Kodi 22) imports clock_settime (d7nUj1LOdDU) from
libkernel.prx, one reference, after sigwaitinfo in its import order.
Predicted stop once the imports ahead of it are covered (not a run):

  FAIL: unresolved ELF import d7nUj1LOdDU from libkernel.prx

The NID is computed with tools/nid_names.py and matches aerolib.

Follows FreeBSD 11.4 sys/kern/kern_time.c sys_clock_settime and
kern_clock_settime (8c639945a44588f6cfdddfb057bb5aad88735da0) for a
root caller: EFAULT for a null time (copyin), EINVAL for any clock but
CLOCK_REALTIME and for a negative tv_sec or a tv_nsec outside 0 to
999999999. A valid CLOCK_REALTIME request throws with the requested
time named, since the host clock must not be set and no guest clock
offset is kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Co-authored-by: David <david@example.com>
2026-10-13 14:57:46 +01:00
p3nicillin 72a685b906 feat(libkernel): report _SC_CLK_TCK through sysconf
The Python in PS5MC (PPSA99420) reads sysconf(_SC_CLK_TCK) while
importing its posix module and failed with "cannot read
ticks_per_second". It returns FreeBSD's CLK_TCK, 128.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:53:24 +01:00
Mouad-Scriptz-0 326e0bc29f fix(agc): raise the graphics end-of-pipe event when a flip completes
A flip packet now delivers the queue's end-of-pipe event once the work
recorded before it completes, as a RELEASE_MEM with INT_SEL does. Minecraft
(PPSA17221) registers the graphics event, never requests an interrupt in its
releases, and waits on the event after every flip while holding its render
lock, so it presented one frame and then waited forever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 15:49:45 +02:00
Adrià Franch 7a8e1b2559 docs(debt): restore UTF-8 characters corrupted in shader.md
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 15:48:15 +02:00
eljackjoestar-cmd 57b2dc8584 perf(shader): reclaim unreachable cached source IR values 2026-10-13 15:48:04 +02:00
Adrià e152ba8f69
Merge pull request #4209 from karanvora2599/fix/shader-cube-gradients
fix(shader): keep cube image gradients out of the face coordinate bias
2026-10-13 15:45:43 +02:00
Adrià 6cd052848a
Merge pull request #4210 from karanvora2599/fix/agc-depth-before-shader
fix(agc): test depth before pixel shaders that ask for it
2026-10-13 15:44:24 +02:00
Karan Vora 0998eb4f23 fix(shader): declare the GDS buffer coherent
GDS is one memory for the whole GPU on AMD hardware: every work-group's
ds_read and ds_write with gds see the others' writes, with no cache in
between. The recompiler keeps it in a storage buffer that was not decorated
Coherent, so on a GPU whose L1 caches storage buffer loads, NVIDIA's from
Turing on among them, a plain GDS read could return a value cached before
another work-group's GDS write. Atomics were not affected. The gds buffer is
now declared Coherent.

verifyCoherentGds in agc_shader_memory compiles a ds_read_b32 and a
ds_write_b32 on GDS and checks that the gds buffer is Coherent.
2026-10-13 15:43:31 +02:00
Adrià 2cf23fcc56
Merge pull request #4114 from Celegans12/fix/agc-tessellation-num-patches
fix(agc): bound tessellation patch records by the draw's patch count
2026-10-13 15:40:48 +02:00
Adrià 6c0f83c7cf
Merge pull request #4211 from karanvora2599/perf/shader-bindless-table-once
perf(shader): search a bindless image table once per access
2026-10-13 15:39:50 +02:00
Adrià Franch 36eb627051 fix(libSceFiber): throw when a fiber's host stack cannot be set up
No fallback to the title's context: a failed mmap or mprotect throws,
as on Windows.
2026-10-13 15:37:30 +02:00
fdfawef324 81364388a0 fix(libSceFiber): run fibers on host stacks on Windows
#4265 gives each fiber an mmap'd host stack on Linux and leaves Windows
as it was: there a fiber still runs host calls inside the context the
title passed to sceFiberInitialize. Demon's Souls packs its 16 KiB job
fiber contexts back to back. A job that calls
sceAgcCreateInterpolantMapping reaches shader preparation on the fiber
(Driver::ResolveShaderAbi, PrepareRegistered, the RDNA decoder,
BuildShaderStageInputInfo), goes more than 16 KiB deep and overwrites
the frames at the top of the context below it. On the loading screen
after the opening cinematic the fiber that owns that context then
failed the title's stack check, or returned to 0x7fffffff.

On Windows each fiber now gets a host stack from VirtualAlloc, the
context size plus 1 MiB, committed like the 1 MiB host margin of guest
threads, with a PAGE_NOACCESS page at the bottom. While the fiber runs,
the TIB stack base and limit and the deallocation stack describe that
host stack, as Windows does for its own fibers, so stack probes and
GetCurrentThreadStackLimits see the stack the fiber runs on. A host
stack that cannot be allocated, protected or released throws instead
of falling back to the context.

agc_driver_gpu_map_on_fiber: GetCurrentThreadStackLimits on a fiber now
returns its host stack, which has room for the import, so on Windows a
GPU mapping made on a fiber imports on the fiber's stack instead of a
host thread. The fiber's context is still not written.

Test: on Windows, fiber checks that the TIB stack bounds inside a fiber
cover its host stack, at least the context plus 256 KiB. With #4265
alone it fails at "the thread's stack bounds are the fiber's host
stack", and without that check at "a host call from a 16 KiB fiber
writes nothing below its context"; with this change fiber and
agc_driver_gpu_map_on_fiber pass (Windows Server 2025, RTX 3060,
WinLibs GCC 15.2.0, Release).

Demon's Souls (PPSA01342) on that host: at the loading screen the two
16 KiB job fibers above the overwritten one now use 17,592 and 16,936
bytes of their host stacks, the title's stack check no longer fails
there, and the load goes on until a separate Vulkan allocation failure
(vkAllocateMemory of a 4 MiB host-visible buffer, out of device memory).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:37:23 +02:00
Joey Fisher 794714983c fix(libSceFiber): run fibers on host stacks with room for host calls
Host code that a guest fiber calls runs on the fiber's stack. Demon's
Souls (PPSA01341) gives its job fibers 16 KiB contexts packed next to each
other, and a fiber that reaches shader preparation through
Driver::ResolveGraphicsStagesAbi goes about 21 KiB deep when SPIRV-Tools
validates the module. That overwrites the saved frame of the fiber below,
and the next switch to that fiber pops garbage into rbp, rbx, r12-r15 and
rip (a jump to 0x1a in the observed crash).

Fibers now run on an mmap'd host stack of the context size plus 1 MiB,
with a guard page at the bottom, the same margin guest threads get since
#2647. The guest context is no longer written. Context size checks fill
and scan the top context-size bytes of the host stack, so
size_context_margin keeps its meaning. If the mapping fails the fiber
runs in its context as before. Windows is unchanged: it pins and
publishes the guest context as the stack, and that path is not covered
here.

The fiber test now runs a 256 KiB host call from a 16 KiB fiber placed at
the top of a guard-filled area and checks nothing below the context was
written. It fails on main.
2026-10-13 15:37:23 +02:00
Adrià 58f7242ea2
Merge pull request #3934 from MarcZX100/codex/carry-setlocale-category
fix(libc): reject invalid setlocale categories
2026-10-13 15:35:56 +02:00
Adrià 3a60a0d054
Merge pull request #4011 from karanvora2599/perf/shader-f16-compare-any-nan
perf(shader): skip the f16 NaN rebuild for operands that are only compared
2026-10-13 15:34:02 +02:00
Adrià Franch 1ab578a3f7 test(libc): allow the __cxa_thread_atexit export to call the MinGW registration
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 15:33:28 +02:00
Adrià Franch 9c383e3295 fix(libc): ignore the guest DSO of thread atexit on Windows in the shared bridge
MinGW's __cxa_thread_atexit asserts dso is null or its own __dso_handle, so every guest export that forwards a guest module pointer needs the null rule, not only LibcInternalExtCxaThreadAtexit. The test now passes a non-host DSO on Windows too.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 15:33:22 +02:00
MarcZX100 90c64af925 fix(libc): export __cxa_thread_atexit 2026-10-13 15:33:22 +02:00
Adrià 0c05d3cfc7
Merge pull request #4105 from MarcZX100/codex/carry-execv-execve-3558
feat(libkernel): add execve as a throwing export
2026-10-13 15:31:29 +02:00
Akul Tyagi b4e2ee0a13 feat(libc): implement __powidf2
The H+8UBOwfScI import was a throwing stub (libcCyberUnknown08). It is
the NID of "__powidf2", the double-precision integer-power helper that
complements __powisf2.
2026-10-13 19:00:24 +05:30
Adrià 5b08344585
Merge pull request #3907 from GalaxyGamingFR/fix/progress-read-encoding
fix(progress): read cmake and shader tables as utf-8
2026-10-13 15:27:02 +02:00
Adrià Franch 3973e5ad77 docs(technical-debt): record the hardware measurement of cube image gradients
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 15:26:05 +02:00
Karan Vora 73eaa1f18b fix(shader): keep cube image gradients out of the face coordinate bias
image_sample_d and image_sample_cd on a cube pass their gradients through the
same conversion as the face coordinates, which subtracts 1.0 to move AMD's
[1, 2] face range to [0, 1]. A gradient is a difference of coordinates, so the
bias turned a one-texel gradient into one of about -1 and the sample read the
smallest mip. Gradients now skip the bias.

agc_driver_image_sample_cube_derivatives samples every face of a 16x16 cube at
five levels through image_sample_d and checks the level and face it read.

TechnicalDebt records that the gradient handling follows Mesa and was not
measured on hardware.
2026-10-13 15:26:04 +02:00
Adrià e22a77eba2
Merge pull request #3901 from GalaxyGamingFR/fix/conventions-agent-file-case
fix(conventions): match agent instruction files case-insensitively
2026-10-13 15:26:01 +02:00
Adrià 3faa9cb400
Merge pull request #5103 from ErenYeager1836/carry/audioout-enable-chat
feat(libSceAudioOut): implement sceAudioOut2EnableChat
2026-10-13 15:22:57 +02:00
Adrià a252fcd75f
Merge pull request #4012 from queeeef/perf/agc-zero-dword-fill
perf(agc): handle the zero dword fill kernel as a buffer fill
2026-10-13 15:21:00 +02:00
Adrià Franch b81ababfc6 docs(technical-debt): record DEPTH_BEFORE_SHADER in debt/libSceAgcDriver.md 2026-10-13 15:19:51 +02:00
Karan Vora 320bd37c92 fix(agc): test depth before pixel shaders that ask for it
DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER (bit 12) is what a pixel shader compiled
for early depth/stencil sets: the depth and stencil tests and writes happen
before the shader runs, even when it kills pixels or writes memory. The bit
was accepted but never read, so such shaders got EarlyFragmentTests only when
Z_ORDER was EARLY_Z_THEN_LATE_Z and they had no kill or exports. Their killed
pixels then left depth unwritten, and occluded pixels still wrote memory.
EARLY_Z_THEN_RE_Z (Z_ORDER 3) was not treated as early either.

DecodePixelStageInfo now sets earlyZ for DEPTH_BEFORE_SHADER, and for Z_ORDER
1 or 3 under the existing kill and export conditions, including the stencil
reference export. The backend no longer repeats those conditions; it keeps
only the alpha-to-coverage one, so a shader with alpha-to-coverage still runs
its tests late. earlyZ keeps its place in the request, so the serialization
version does not change.

earlyFragmentTestsTests in agc_driver_graphics checks which DB_SHADER_CONTROL
values declare EarlyFragmentTests, and that alpha-to-coverage keeps Z_ORDER 1
and DEPTH_BEFORE_SHADER late, directly and after a serialization round trip.
stencilRefExportTests checks that DEPTH_BEFORE_SHADER with a stencil reference
export still decodes as early.

TechnicalDebt records that the bit follows Mesa and that its combination with
a Z export, a mask export, a stencil export or alpha-to-coverage was not
measured.
2026-10-13 15:19:33 +02:00
Adrià Franch cdf4b174f4 fix(libkernel): keep the execv export and its debt line
The rename to execve dropped execv, which PPSA97358 imports.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 15:19:19 +02:00
MarcZX100 77016caa24 fix(libkernel): limit execve to unsupported behavior 2026-10-13 15:19:19 +02:00
David 4f1ae1cc3a feat(libkernel): implement execv and execve as throwing exports
XPSemu PPSA97358 imports both from libkernel.prx; with the imports ahead
of them covered, its predicted next stops (relink registry order, not an
observed run) are:

  FAIL: unresolved ELF import KaK52lYTBIs from libkernel.prx  (execv, 7 call sites)
  FAIL: unresolved ELF import kdguLiAheLI from libkernel.prx  (execve, 4 call sites)

Today the title stops earlier at the observed
  FAIL: unresolved ELF import aRo9AhFUXcM from libkernel.prx  (sigsetjmp, merged in #3486)

As for execvp (#2827), the process image cannot be replaced here, so both
throw with the execvp message pattern once FreeBSD 11's execve would start
loading the image (freebsd-src 8c639945, sys/kern/kern_exec.c
exec_copyin_args, do_execve, exec_check_permissions; lib/libc/gen/exec.c
execv passes environ). Before that they fail as FreeBSD does: EFAULT for a
null argument vector or path, ENOENT and ENOTDIR from the lookup as stat
reports it, EACCES for a path that is not a regular file. Paths of
PATH_MAX bytes or more and strings over ARG_MAX throw instead of guessing
the console's limits. The execv calls in XPSemu's sources run in a fork
child (net/tap.c), and its fork fails with ENOSYS (ZiZc3/XPSemu 8eec07b4,
ps5/compat/libc_missing.c, ps5/build-title.sh --defsym).

NIDs computed with tools/nid_names.py and confirmed in aerolib.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 57f0fa37132bc6c78bc02282044a00e2db859484)
2026-10-13 15:19:19 +02:00
xXPBSXx 79e162110a fix(libkernel): report SO_ERROR and the other SOL_SOCKET options like FreeBSD
getsockopt and setsockopt on a libkernel socket knew four SOL_SOCKET options
(SO_REUSEADDR, SO_BROADCAST, SO_SNDBUF, SO_RCVBUF) and failed every other
one with ENOPROTOOPT. The Mound's EOS SDK reads SO_ERROR on two of its
sockets after a connect, which therefore always failed.

The SOL_SOCKET options now follow FreeBSD 11.4 sosetopt and sogetopt:
- SO_ERROR returns the pending error of the socket as a FreeBSD errno and
  reports it once (Winsock does not clear it on read, so the library does).
- SO_TYPE, SO_ACCEPTCONN, SO_KEEPALIVE, SO_DONTROUTE, SO_OOBINLINE and
  SO_LINGER are read from and set on the host socket; the boolean options
  read back as their own bit, as FreeBSD returns so_options & option.
- SO_DEBUG, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP and SO_NOSIGPIPE stay
  ENOPROTOOPT until a title needs one.
- A value is read from its first int and fails with EINVAL when shorter, a
  getsockopt buffer is filled up to its length (sooptcopyin, sooptcopyout),
  SO_SNDBUF and SO_RCVBUF below 1 fail with EINVAL, and SO_LINGER outside
  0..65535 seconds fails with EDOM.

A connect on a non-blocking socket now fails with EINPROGRESS on Windows too
(it was EWOULDBLOCK, which a title waiting for the connection does not
expect).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 15:18:29 +02:00
Adrià 6ac63bf0cb
Merge pull request #5104 from Feuris/feat/libc-wcscat
feat(libc): implement wcscat
2026-10-13 15:18:07 +02:00
Adrià 5aaa394cff
Merge pull request #5080 from ErenYeager1836/carry/libSceAgc-amm-semaphore
feat(libSceAgc): implement sceAgcSetAmmSemaphoreMemory and sceAgcGetSemaphoreLabel
2026-10-13 15:17:12 +02:00
Adrià 1e7f4967ed
Merge pull request #3743 from YossiYad/carry/ampr-pad-launch
feat(libSceAmpr): implement the std::_Pad thread launch helpers
2026-10-13 15:15:37 +02:00
Adrià 21bdab007a
Merge pull request #1312 from TibixDev/pr/incremental-draw-fingerprints
perf(agc): maintain draw fingerprints on register writes
2026-10-13 15:15:17 +02:00
Adrià Franch 1a0de9e91f fix(libc): drop unused clocale include
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 15:13:01 +02:00
MarcZX100 2ec20dbd37 fix(libc): validate guest setlocale categories 2026-10-13 15:12:57 +02:00
MarcZX100 5eb99f8b65 fix(libc): reject invalid setlocale categories
(cherry picked from commit 099fa80b53eba577d29a0bfff672dc4f9c160ec1)
2026-10-13 15:12:56 +02:00
Karan Vora 352bf304fe perf(shader): skip the f16 NaN rebuild for operands that are only compared
With float16DenormPreserve, EmitF16BitsToF32 rebuilds the bits of a NaN
half after UnpackHalf2x16, because NVIDIA returns the default NaN and
Intel quiets it. When every use of the converted operand is an f32
comparison or a NaN test, directly or through abs and neg, only whether
it is NaN matters, so the conversion returns UnpackHalf2x16's result as
is. v_cmp_*_f16 goes from 11.5 to 4 SASS on an RTX 4060.

agc_driver_f16_to_f32_unpack adds v_cmp_u_f16 and v_cmp_lt_f16 over all
65,536 halves with each decode the device supports. Its SPIR-V check
requires the v_cvt_f32_f16 conversion to rebuild the NaN bits and the
compared conversions not to.
2026-10-13 15:11:54 +02:00
Aarav 2f453805aa fix(libSceSystemService): contain converted executable paths 2026-10-13 18:40:19 +05:30
Adrià c98fda869a
Merge pull request #4015 from sherySJ/agent/libc-time-eoverflow
fix(libc): keep the caller errno when localtime cannot represent a time
2026-10-13 15:10:07 +02:00
Adrià 4008e68e39
Merge pull request #3919 from GalaxyGamingFR/fix/stubgen-stdout-newlines
fix(stubgen): write LF to stdout on Windows
2026-10-13 15:09:35 +02:00
Adrià d5835a613b
Merge pull request #5102 from Dripl1x/codex/generic-error-category
feat(libc): implement generic error category messages
2026-10-13 15:08:29 +02:00
Adrià Franch 76075edd38 test(libSceAudioOut): define TestEnableChat; drop stale EnableChat unknown-signature line
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 15:06:32 +02:00
screamm 50cd318bb8 feat(libSceAudioOut): implement sceAudioOut2EnableChat
Palworld (PPSA23126, #2648) stops at start-up on the export, imported by
its bundled libdiscord_partner_sdk.prx:

    FAIL: unresolved ELF import oiOi7vgPfAY from libSceAudioOut.prx

PPSA28416 imports it from its bundled party-ps5-shipping.prx as well. The
call itself has not been observed yet.

SharpProspero (5dfdb4b0, Interop/Audio/AudioOut2.cs:460-464) declares
`int sceAudioOut2EnableChat()`, "Allows chat audio alongside the
application's own", and the ps5-payload-dev SDK stub (f5d40f02,
sce_stubs/libSceAudioOut.c:62) exports it as well. There is no context or
handle to validate. The host mixes no chat audio and AudioOut2 has no chat
port, so the call records that chat audio is allowed (traced once with
APS5_TRACE_AUDIOOUT2) and returns 0. Whether the console fails it without
a chat session or when repeated is recorded in technical debt, and the
PPSA28416 "signature unknown" entry is dropped.

NID oiOi7vgPfAY matches tools/nid_names.py and the shadPS4 aerolib list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 15:06:27 +02:00
Adrià Franch 6ba5d68e07 docs(debt): move the AMM semaphore line away from the end of Functional 2026-10-13 15:01:48 +02:00
David 352d04e8dc feat(libSceAgc): implement sceAgcSetAmmSemaphoreMemory and sceAgcGetSemaphoreLabel
sceAgcSetAmmSemaphoreMemory(memory, sizeInBytes) registers the AMM semaphore memory once and zeroes the first 8 bytes of each 32-byte label; sceAgcGetSemaphoreLabel(index, label) writes memory + 32 * index for an index inside the registered memory.

Unblocks New Joe & Mac Caveman Ninja (PPSA02801) which imports both symbols. Matches OpenAGC and sharpemu agreements on signatures, 16 KiB alignment, 32-byte stride, and zeroed label counters.
2026-10-13 15:01:43 +02:00
Kikoarl e5f0938409 feat(ces): implement sceCesUcsProfileInitSJis1997Cp932 2026-10-13 15:00:31 +02:00
Yossi Yadgar dd1deb6965 fix(ampr): raise std::_Pad failures as the guest std::system_error
A failing _Pad primitive threw a host std::runtime_error, which no guest
catch can see. threadcall.cpp calls the checked wrappers of xthread
(_Mtx_lockX, _Cnd_waitX, _Thrd_startX), which pass a non-success code to
_Throw_C_error: _Thrd_nomem and _Thrd_timedout become
resource_unavailable_try_again, _Thrd_busy device_or_resource_busy and
_Thrd_error invalid_argument, raised through _Throw_Cpp_error, which main
now implements. A failing thread start is _Thrd_startX's
resource_unavailable_try_again.

_Release now wakes the launching thread with _Cnd_signal, as
threadcall.cpp does; libc has it since the pad was written.

guest_ampr_threads checks, in a forked child whose guest terminate
handler reports the thrown object, that _Release on the thread still
holding the pad's mutex throws std::system_error with EBUSY. It fails
with the previous host exception and with busy mapped to
invalid_argument.
2026-10-13 15:59:28 +03:00
melihcan 0afa191660 test(libSceConvertKeycode): cover sceConvertKeycodeGetImeKeyboardType
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:59:08 +02:00
xXPBSXx c6a4223420 fix(agc): sample 10_10_10_2 SNorm and SInt textures
Guest texture formats 51 (SNorm) and 55 (SInt) of the 10-bit packed group were missing from the texture lookup and the descriptor format table, so a descriptor using them threw 'unsupported guest texture format' while the UNorm (50) and UInt (54) siblings worked. Map them to A2B10G10R10_SNORM_PACK32 / A2B10G10R10_SINT_PACK32, classify them for sampling (float / signed integer), and list SINT among the integer formats.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 14:56:03 +02:00
Aarav 6f0f7c7461 fix(relinker): reject relocatable ELF conversion inputs 2026-10-13 18:23:31 +05:30
Yossi Yadgar 8e3b2a06f2 fix(ampr): exclude the private pad entry from the export inventory 2026-10-13 15:51:00 +03:00
Yossi Yadgar 49d8cf3e61 test(ampr): exercise simultaneous pad launches and destroyed storage 2026-10-13 15:51:00 +03:00
David d991cdccf9 feat(libSceAmpr): implement the std::_Pad thread launch helpers
Implement _ZNSt4_PadC2Ev, _ZNSt4_PadD2Ev, _ZNSt4_Pad7_LaunchEPP7pthread
and _ZNSt4_Pad8_ReleaseEv, which Deathloop (PPSA01670) and Minecraft
Legends (PPSA05510) import from libSceAmpr for the std::thread launch path.

The algorithm and object layout follow Dinkumware V6.00 thr/xthread and
thr/threadcall.cpp of Visual C++ 2013 (ganboing/crt.vc12 f1d47126): a
vtable pointer whose first slot is the pure virtual _Go(), then the
condition and mutex handles and bool _Started (0x8, 0x10, 0x18), with the
console's pointer-to-slot handles of libc Threads.cpp (#3079). The pad
uses libc's _Cnd_*/_Mtx_* exports, starts the guest thread with
pthread_create and an entry that calls _Go() and returns its result as
the exit code, as _Call_func does. The unverified layout, thread
attributes and error reporting are recorded in TechnicalDebt.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 15:51:00 +03:00
Adrià Franch a37ee5ff2c fix(libc): keep the caller errno when the Windows tm_year range overflows
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 14:50:07 +02:00
sherySJ b1cf01464a fix(libc): keep the caller errno when localtime cannot represent a time 2026-10-13 14:50:03 +02:00
Adrià Franch dd49e1df93 build(agc): put libc's directory on PATH for the register key test on Windows
The sibling AGC driver tests get this through the agcTest loop; without it the test exits 0xc0000135 (DLL not found) on Windows.
2026-10-13 14:49:31 +02:00
Adrià Franch 89414d3039 build(agc): link register key test with libc and libkernel like its sibling tests
The Windows build failed to start it (0xc0000135, DLL not found).
2026-10-13 14:49:24 +02:00
TibixDev 5942b8c7f1 perf(graphics): track draw register fingerprints incrementally
Track mutations against the current draw dependency mask and preserve device and shader registry identity in the draw key. Validate mutations, projected masks, PM4 packets and prepared graphics against full recomputation.

Written with AI assistance.
2026-10-13 14:49:24 +02:00
GalaxyGamingFR 1c32fd93c8 fix(progress): read cmake and shader tables as utf-8
scan_library and collect_shaders read CMakeLists.txt, the ISA table and the opcode header with the locale codec. On Windows that is cp1252, so a UTF-8 file containing U+2010 or U+2192 raised UnicodeDecodeError. These reads now pass encoding="utf-8" and errors="ignore", and a subprocess test with an ASCII default codec covers it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq
2026-10-13 14:48:50 +02:00
GalaxyGamingFR b12d6f5394 fix(conventions): match agent instruction files case-insensitively
The repository check compared the file name to a lower-case set, so a
file added as CLAUDE.md, Agents.md or GEMINI.md passed the check while
claude.md was rejected. Compare the lower-cased name so every spelling
of an agent instruction file is reported.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq
2026-10-13 14:48:43 +02:00
screamm 8b9529b99a fix(libScePosixForWebKit): keep the getservbyname result trivially destructible
The per-thread servent storage held std::string and std::vector
members, so getservbyname_nid_postfix registered a thread_local
destructor through __cxa_thread_atexit. On Windows that destructor runs
after emulated TLS freed the object, and prx_thread_local_destructors
(#4029) rejects libScePosixForWebKit.prx for it.

The result now lives in a thread_local struct of fixed-size char arrays
and an alias pointer array, sized for the longest name (submission),
alias (www-http), protocol (sctp) and alias count in the embedded table;
static_asserts check the sizes against the table and that the struct is
trivially destructible. The guest servent layout and the per-thread
result FreeBSD's getservent.c keeps in its NSS TLS state are unchanged.
Nothing is allocated, so the ENOMEM path is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 15:43:05 +03:00
screamm 15bbc19501 feat(libScePosixForWebKit): implement getservbyname
Implement getservbyname (H1bArJPagV0) in libScePosixForWebKit. Cloud
Play (PPSA99600) imports it from this library only (registry index 408,
call site 0x1042124); once fnmatch (#3545) is in, its predicted load
stop is:

    FAIL: unresolved ELF import H1bArJPagV0 from libScePosixForWebKit.prx

The ps5-payload-dev SDK stubs (f5d40f02, sce_stubs/) declare
getservbyname in libScePosixForWebKit.c and not in libkernel.c or
libSceLibcInternal.c. The call site is the service-name branch of a
getaddrinfo inside the title: a non-numeric service is looked up with
protocol "tcp", "udp" or "sctp" and the port is read big-endian at
offset 16 of the result, which fixes the servent layout used here
(name, aliases, 32-bit port, protocol at 0, 8, 16, 24).

The guest has no /etc/services and the console's file is unknown, so the
function searches 37 lines of FreeBSD 11.4 etc/services (8c639945):
ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap,
https, rtsp, submission, imaps and pop3s with their aliases and
protocols, in file order. None of their names or aliases appears on an
earlier line of the full file, so these lookups answer as the full file
would. The search follows files_servent in lib/libc/net/getservent.c
(8c639945): exact, case-sensitive match on the name or an alias, a null
protocol takes the first match, the port in network byte order, the
result in per-thread storage like getserv's NSS TLS state. Any other
name returns null without touching errno, as for a name the file lacks.
The repository's getaddrinfo still resolves service names through the
host, so there was no table to share; this is recorded in
TechnicalDebt.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 15:43:05 +03:00
Avijit Singh 48ea54ed3e fix(relinker): include output stream header
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 18:05:37 +05:30
Avijit Singh 1796482721 docs(relinker): clarify skipped SELF modules
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 18:04:49 +05:30
Avijit Singh f23c5c2fd2 test(relinker): cover SELF skip warnings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 18:04:49 +05:30
Avijit Singh 82f24e44a2 fix(relinker): warn on skipped SELF modules
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 18:04:49 +05:30
Romans Pokrovskis 44c4b3f8bf fix(agc): admit inactive covered-centroid mode
Admit PA_SC_AA_CONFIG bit29 only without active perspective or linear centroid inputs, or with a skipped pixel program. Active/unspecified centroid inputs and unrelated AA fields remain refused. Source: pinned PS5_Mesa 0b2d6d1a GFX10.3 schema/RADV producer; existing ENA/ADDR intersection. Console covered-centroid interpolation remains unmeasured.

Full build/all 124 runtime libraries, conventions and registration pass. Final Windows suite: 965 pass, zero fail, seven skips of 972. Isolated state test passes 20 repeats and fails 5/5 with main State.cpp; four guard mutants fail 5/5 each. Original two title register captures reproduce the main refusal and pass this check with candidate, five times each. NVIDIA relevant groups pass; AMD draw-cache failure also occurs on unchanged main and is reported in issue4435.

AI-assisted: Codex.
2026-10-13 15:34:41 +03:00
boykopovar 0f2683cb7d
Merge pull request #5135 from boykopovar/feat/web-relinker-ui
feat(ci): add relinker wasm build ci
2026-10-13 15:31:28 +03:00
Adrià c827e11872
Merge pull request #4086 from hugodearaujo/fix/setschedparam-console
fix(libkernel): check and keep the scheduling policy of a thread
2026-10-13 14:29:15 +02:00
Adrià afbc22fb1a
Merge pull request #4140 from veosett/fix/relinker-windows-fixed-base
feat(relinker): support fixed Windows guest image bases
2026-10-13 14:26:54 +02:00
Adrià 94d9957725
Merge pull request #4078 from MarcZX100/codex/carry-ssl-connections-3766
fix(libSceSsl): add missing verification callback export
2026-10-13 14:26:09 +02:00
Adrià a164b1d711
Merge pull request #1359 from TibixDev/pr/copied-write-ordering
fix(agc): order direct reads after copied writes
2026-10-13 14:25:22 +02:00
Aarav bc014a887c fix(relinker): reject load file sizes beyond memory size 2026-10-13 17:55:11 +05:30
SandstromPL d34b78890d fix(savedata): return no saves for a search with another title's ID
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:53:29 +05:30
Adrià 398ccaaf96
Merge pull request #5108 from adriadam10/ci/agc-graphics-tests-link-driver
build(agc): link the graphics and BDA device tests against libSceAgcDriver
2026-10-13 14:19:16 +02:00
Adrià bc0375c72a
Merge pull request #5130 from adriadam10/ci/windows-keep-f16-unpack
ci(windows): build the f16 unpack execution test, it runs without a device
2026-10-13 14:18:44 +02:00
Adrià a60f8d940b
Merge pull request #5048 from MarcZX100/codex/libc-openlog-closelog
fix(libc): implement openlog and closelog state
2026-10-13 14:18:31 +02:00
Adrià Franch 5181b30d79 ci(windows): build the f16 unpack execution test, it runs without a device
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 14:18:28 +02:00
boykopovar 8485298e46
Merge pull request #5114 from boykopovar/feat/web-relinker-ui
feat(pages): add web relinker ui
2026-10-13 15:16:10 +03:00
Adrià a2a3a3a94d
Merge pull request #4062 from danzaio/fix/windows-log-without-standard-handles
fix(relinker): log Windows output when standard handles are missing
2026-10-13 14:11:42 +02:00
Adrià 96f3e9fe5d
Merge pull request #4041 from ee6aba54/fix/agc-cmask-unknown-codes
fix(agc): keep the texels of CMASK tiles with codes other than 0 and 15
2026-10-13 14:11:16 +02:00
Adrià Franch 14f2092c9e build(agc): link the graphics and BDA device tests against libSceAgcDriver
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 13:53:51 +02:00
Adrià 14036cb0e8
Merge pull request #5107 from adriadam10/ci/linux-mesa-shader-cache
ci(linux): keep lavapipe's shader cache between builds
2026-10-13 13:51:53 +02:00
Adrià Franch aef5972ff5 ci(linux): keep lavapipe's shader cache between builds
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 13:51:10 +02:00
Celegans12 247162dc4f feat(agc): read hull patch constants in tessellation domain programs
TessellationLowering refused every domain-program read of the off-chip
ring outside its patch's control points, so domain programs that read
the hull's patch constants, like the quad terrain domain in Ghost of
Yotei at 0x8000 - 208 * (rel + 1), aborted.

Such reads now lower to a new PatchInput kind and become Patch inputs.
Hull outputs and domain inputs place a constant by its off-chip address
modulo the record stride, in a record-sized array at location
ceil(hsStride / 16); each program derives both on its own, so the two
interfaces match although they compile separately. Domain reads at an
address that does not move with the relative patch, across more than
one record or inside the control-point region still throw, as does a
domain program that reads patch constants but no control point. Hull
patch-constant writes now take the same bound: above the control points
of all NUM_PATCHES patches, not just one patch's.
2026-10-13 13:50:53 +02:00
Celegans12 25a83722c7 fix(agc): bound tessellation patch records by the draw's patch count
TessellationLowering proved ring and LDS addresses affine over a
relative patch index of up to 256. A hull that lays its patch-constant
records out downwards, at 0x8000 - 208 * (rel + 1) like the quad
particle hull in Ghost of Yotei, then reached a negative address for
the last slot and aborted as "not affine". The relative patch index of
hull and domain programs stays below VGT_LS_HS_CONFIG.NUM_PATCHES
(bits 7:0, patches per threadgroup), which is 63 for that draw.

DecodeShaderStages now carries NUM_PATCHES as patchesPerGroup in the
tessellation configuration (draw keys, hull and domain cache keys,
recompile request version 22) and refuses zero, and the lowering bounds
the relative patch index by it. Configurations without it keep 256.
2026-10-13 13:50:27 +02:00
Adrià ff8fe4dbc1
Merge pull request #3988 from karanvora2599/perf/shader-f16-unpack-half
perf(shader): convert f16 operands with UnpackHalf2x16 where f16 denormals are kept
2026-10-13 13:48:59 +02:00
Adrià c63dd4442c
Merge pull request #5106 from adriadam10/ci/agc-mesh-tests-link-driver
build(agc): link agc_driver_mesh_tests against libSceAgcDriver
2026-10-13 13:47:54 +02:00
Adrià Franch f20a32bec3 build(agc): link agc_driver_mesh_tests against libSceAgcDriver
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 13:46:45 +02:00
Dean ce8f7eeb3f
Merge pull request #4344 from Roojool/contrib/round2-c19
fix(tools): use configured Ninja for test registration audit
2026-10-14 00:46:35 +13:00
Adrià 84e2a8c0b3
Merge pull request #5105 from adriadam10/ci/windows-device-test-stubs
ci(windows): build only the execution tests that run without a Vulkan device
2026-10-13 13:45:23 +02:00
AlxcBTW 230b5c2683 perf(agc-driver): join the imports of adjacent large ranges when a buffer is bound across them
8 MiB window, so a range larger than the window is always imported alone. A
buffer bound across two such ranges then has no import and is copied whole on
every use: PPSA25344 binds a 368 MiB buffer across a 320.75 MiB and a 47.3 MiB
range and copies it on almost every frame.

When UploadFinish meets such a region, it imports the run of the ranges the
region crosses, provided every one of them is larger than the window, and
retires their own imports. It does this before any region of the build
confirms an import, so the retire moves the epoch ahead of every pointer the
build would use. A range that large is registered whole, not grown in 64 KiB
pieces like a heap, so the joined import is not re-imported as a heap grows; it
stands in refreshImports while exactly its ranges stand. A later join that
reaches a joined import takes it in, so two joins that share a range never
retire each other in turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:45:21 +02:00
Adrià Franch 1137c57572 ci(windows): build only the execution tests that run without a Vulkan device
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 13:44:08 +02:00
Adrià 99e4dfefe7
Merge pull request #5100 from adriadam10/ci/dispatch-gate-matrices
ci(build): run relinker and context matrices on dispatch only when touched
2026-10-13 13:42:58 +02:00
Niko 44767a7b97 perf(libc): size each write-watch query to the region it covers
WindowsMappings::Collect handed GetWriteWatch the whole remaining output
capacity for every private region it walked. The call's cost follows that
capacity, not the size of the range: on Windows 11 (26200) a query of one
16 KiB region took 0.35 us with room for 4 entries and 6.5 us with room
for the 65536 the write tracker passes. Flexible memory is committed in
16 KiB allocations, one region each, so a resetting walk over 33 MiB of it
took 14.7 ms, 5.8 times a memcmp of the same bytes.

Each query now gets room for the pages its region spans, which is the most
it can report, so every walk returns the same pages; the 33 MiB walk takes
1.23 ms. guest_memory checks that a walk over eight 16 KiB allocations
reports every stored page once, in order, with room for 32, 5 and 1 pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:40:33 +02:00
Adrià 2f969f147b
Merge pull request #3973 from MarcZX100/codex/carry-rudp-end-terminate
fix(libSceRudp): correct End and Terminate functions
2026-10-13 13:40:04 +02:00
Adrià e57a793958
Merge pull request #3911 from GalaxyGamingFR/fix/pr-overlap-own-branch
fix(tools): drop the branch's own pull request in pr_overlap --branch
2026-10-13 13:39:14 +02:00
Adrià Franch 3c273e7081 ci(build): run relinker and context matrices on dispatch only when touched
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 13:39:04 +02:00
Dean 9fa50827dd
Merge pull request #4532 from DavidSamir/fix/libc-rename-replace-open-destination-windows
fix(libc): replace an open or read-only destination in rename on Windows
2026-10-14 00:38:14 +13:00
Evgeniy Kazarin e3cef332e6 feat(libc): implement generic error category messages 2026-10-13 14:38:04 +03:00
Feuris cd0e9dccd7 feat(libc): implement wcscat 2026-10-13 08:38:02 -03:00
Dean 18414447b9
Merge pull request #4525 from Lorkydey/fix/poll-priority-windows
fix(libkernel): support POLLPRI on Windows sockets
2026-10-14 00:37:59 +13:00
Dean e09ee165d5
Merge pull request #3651 from MarcZX100/codex/libc-posix-spawn-3624
feat(libSceLibcInternal): implement posix_spawn actions
2026-10-14 00:37:44 +13:00
Dean bc1ded3a18
Merge pull request #4386 from MarcZX100/codex/carry-mlockall-getdtablesize-3628
feat(libkernel): add mlockall export
2026-10-14 00:37:29 +13:00
XairRrR 95f34a0504 fix(agc): report GPU time per presented or dropped frame
The [present] line divided the GPU busy and idle time of a window by the
presentations that got a swapchain image. That time covers the GPU work
of every frame, so with frames dropped for want of an image it read
several times too high (278.9 ms instead of about 52 ms on a hidden
window, reported on #4684). Those two figures are now divided by the
presented and dropped frames; the per-present blit figures are unchanged.
2026-10-13 13:34:19 +02:00
bubblegg 4b7a357bd2 fix(savedata): report a save's size as the mount that created it gave
Every native save reported SAVE_DATA_BLOCKS_MAX blocks, all of them free,
in the sceSaveDataDirNameSearch infos and in sceSaveDataGetMountInfo,
whatever the title created it with and whatever its files held. A save's
size is now the blocks of the sceSaveDataMount3 call that created it,
kept beside the save; a save without a kept size takes that of its next
mount that names one, and otherwise keeps SAVE_DATA_BLOCKS_MAX. Free
blocks are what the save's files leave of it, each file rounded up to
32 KiB blocks, and the blocks and free-blocks sort keys use both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:32:43 +00:00
XairRrR 37d9ef133f fix(agc): drop a presentation instead of waiting for a swapchain image
The presenter acquired the next swapchain image with a 5 s timeout
before completing the guest's flip. With FIFO on Wayland, NVIDIA's
driver hands out about one image per second while the window is hidden
(another workspace or occluded): vkAcquireNextImageKHR blocked for
~1 s per flip, and the title ran at exactly 1 frame per second. Stack
snapshots of Uncharted: Legacy of Thieves Collection (PPSA05684) showed
the presenter in drmSyncobjTimelineWait under AcquireImage in every
snapshot taken during such a stall, and in none outside them.

The acquire no longer waits: VK_NOT_READY or VK_TIMEOUT drops that
presentation, as an out-of-date swapchain already does, and the flip
completes. APS5_PROFILE_DRAW's [present] line counts the drops.

Menu with the window moved to an empty workspace for 50 s: 7.5 fps and
about 65 drops per 10 s while hidden (1 fps before), no drops while
visible.
2026-10-13 13:30:47 +02:00
Adrià e99a1b0aa9
Merge pull request #3903 from GalaxyGamingFR/fix/conventions-body-bom
fix(conventions): read the PR body file with utf-8-sig
2026-10-13 13:30:34 +02:00
Adrià f56d638a5a
Merge pull request #3688 from CouchRiv/fix-libc-acosf-errno
fix(libc): keep the acosf result and leave errno unchanged
2026-10-13 13:29:58 +02:00
p3nicillin aebc3f1480 feat(libc): export tzname
ES-DE in RetroArch (PPSA99169) imports tzname from libSceLibcInternal and
fails to load without it; ICU's uprv_tzname reads it after localtime. It
points at the host's zone names and tzset refreshes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:28:33 +01:00
Dean 5d50886ee4
Merge pull request #2076 from jdmagic/feat/agc-stencil-plane-storage
feat(libSceAgcDriver): take a one-layer 2D array view of a depth surface plane as the plane
2026-10-14 00:11:01 +13:00
Dean 3ee394ea9a
Merge pull request #4543 from coltken95/fix/libc-time
fix(libc): format strftime and asctime as FreeBSD 11 does
2026-10-14 00:10:43 +13:00
Dean 6f7cda184b
Merge pull request #4315 from JAB-dev/feat/agc-dcc-packed-float-clear
feat(agc): encode packed R11G11B10 float DCC fast clears
2026-10-14 00:10:16 +13:00
Dean 50578eb936
Merge pull request #4216 from Mouad-Scriptz-0/perf/guest-memory-registry-lookups
perf(libkernel): find guest allocations and direct mappings by address
2026-10-14 00:09:54 +13:00
Dean 0745fdd72f
Merge pull request #4872 from David-cmrg/fix/systemservice-loadexec-exit-order
fix(libSceSystemService): run guest exit handlers first on LoadExec exit
2026-10-14 00:09:38 +13:00
Adrià c8eb1f52d3
Merge pull request #3898 from GalaxyGamingFR/fix/progress-wrapper-definitions
fix(progress): recognise non-static and noexcept stub helpers
2026-10-13 13:01:36 +02:00
lorkydey d4b59ce9fa fix(agc): accept inactive vertex point size on non-point draws 2026-10-13 12:56:44 +02:00
Kovalev Vladislav 319fa5779a feat(relinker): support fixed Windows guest image bases 2026-10-13 12:55:54 +02:00
Adrià b1328fc0f9
Merge pull request #4100 from nyavana/docs/relinker-help-refresh
docs(relinker): correct help and exit-code usage
2026-10-13 12:54:27 +02:00
hugodearaujo 5c6a9d22c4 fix(libkernel): check and keep the scheduling policy of a thread
On a PS5 (firmware 13.60) pthread_setschedparam and
scePthreadSetschedparam accept only SCHED_FIFO and SCHED_RR with a
priority of 256 to 767 or 896 to 959, return EINVAL otherwise, and
keep the policy for pthread_getschedparam. A created thread takes the
policy of explicit scheduling attributes, or its creator's when it
inherits. Here any policy and priority were accepted and SCHED_FIFO
was always reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:53:19 +02:00
Adrià Franch e34281b558 docs(debt): keep the other SSL stubs in the PPSA07411 entry
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 12:50:47 +02:00
MarcZX100 5d00bd7bbf feat(libSceSsl): add missing verification callback export 2026-10-13 12:50:43 +02:00
Yaash Singhh 4f87ac0b1c fix(libkernel): harden DirectMemory pool bounds and rollback 2026-10-13 10:49:27 +00:00
Dean 075169f7b3
Merge pull request #4404 from eljackjoestar-cmd/codex/demanded-shader-preparation
fix(agc): prioritize demanded shader preparation
2026-10-13 23:42:40 +13:00
Dean 768cd93977
Merge pull request #4082 from Celegans12/feat/agc-quad-tessellation
feat(agc): implement quad tessellation with counter-clockwise output and level clamps
2026-10-13 23:42:25 +13:00
Dean 1d7b075610
Merge pull request #4496 from Kikoarl/perf/agc-gds-dma-and-completion-label-sync
perf(agc): disambiguate completion label overlaps in CopyBuffer
2026-10-13 23:42:11 +13:00
Adrià Franch 3949fb9d44 test(agc): skip copied-direct ordering without Vulkan and find libc on Windows
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 12:34:08 +02:00
TibixDev 29b227bf35 fix(agc): complete copied writes before direct consumers
Wait for overlapping CPU writebacks and completion labels before importing buffers or accepting cached image resources. Cover texture data and compression metadata, retain asynchronous disjoint and GPU-only dependencies, and avoid recursive completion waits.

Reproduced fresh-buffer and cached-image failures with CPU Vulkan before fixing them. Regression covers fresh/reused bindings, dispatch/draw copied writers, texel/metadata updates, labels and no-wait cases; passes CPU Vulkan and RX 6600 synchronization validation. Graphics and guest-memory suites pass. The Skyrim loading GPU fault remains unattributed.

AI-assisted implementation and independent code review.
2026-10-13 12:34:03 +02:00
Dean 53aea1ff34
Merge pull request #5024 from oneandonlydean/up4/storage-store-keeps-untracked-writes
fix(agc): keep CPU writes when a pending storage result is stored over untracked memory
2026-10-13 23:33:08 +13:00
Dean 387b8a0faa
Merge pull request #3763 from eljackjoestar-cmd/codex/dynamic-rendering-layers
fix(agc): render all attachment layers with dynamic rendering
2026-10-13 23:32:53 +13:00
Adrià Franch 99ebc650c3 fix(agc): zero dword fill only for 32-bit output formats
The kernel stores through the V# FORMAT; with an 8- or 16-bit format it writes 1 or 2 bytes per element, which a zero fill of the whole range would also clear between. Same check as MatchDwordPatternFill (format 20-22), with test rows.
2026-10-13 12:29:00 +02:00
jagrat Singh 9efabff1cb feat(libkernel): return ESRCH and EINVAL for unsupported cpuset affinity targets
Allow calling threads to query and set affinity using their own thread ID in addition to -1. Return ESRCH for non-matching thread or process IDs and EINVAL for invalid level/which arguments instead of throwing NotImplemented, matching FreeBSD cpuset semantics.
2026-10-13 15:58:11 +05:30
Daan Dehing 501453573c perf(agc): handle the zero dword fill kernel as a buffer fill
A wave64 compute kernel that stores dword 0 at index group * 64 + thread through a 4-byte
stride buffer is run as a buffer fill. Palworld (PPSA23126) clears its DCC keys with it every
frame; as a shader dispatch its writes were unknown to the DCC key proofs, so every refresh of
those targets waited for the GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:27:46 +02:00
Adrià c7632f036f
Merge pull request #5037 from Feuris/feat/libc-cnd-timedwait
feat(libc): implement _Cnd_timedwait
2026-10-13 12:26:09 +02:00
Adrià a1c2f9fa2c
Merge pull request #3902 from MarcZX100/codex/carry-amm-pool-references-3321
fix(ampr): preserve pooled pages across AMM remaps
2026-10-13 12:25:36 +02:00
Dean Galvin cc3b47dda0 fix(libSceAgcDriver): take a one-layer 2D array plane view as a narrow exception to the non-2D refusal
Main refuses every storage view of a depth or stencil plane that is not
2D. A 2D array view is now taken as the plane only at layer 0 with one
layer; a view with more layers or another base layer still throws.

A one-layer 2D array view replaces a 2D view of the plane like any other
view, so a store back through the replaced 2D view throws. The depth
storage dispatch test runs one-layer views of both planes and checks
that the depth and stencil tests see what they stored, and that a
two-layer view still throws.
2026-10-13 23:17:51 +13:00
jdmagic e4a7cbfc83 feat(libSceAgcDriver): take a one-layer 2D array view of a depth surface plane as the plane
A storage view of a depth or stencil plane was taken for the depth surface
only as a 2D image. A 2D array view of layer 0 with one layer and the
plane's extent and format is the same plane, and now gets the plane's
storage image too; a view with more layers still takes the ordinary path.
The plane's storage image is recreated when the view's dimension changes.

PPSA01289 (Sackboy: A Big Adventure) binds the stencil plane of its
2880x1620 D32S8 surface as a one-layer R8_UINT 2D array from a compute
shader during presentation, and read guest memory instead of the stencil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:15:15 +13:00
Karan Vora e4f212b2e2 perf(shader): convert f16 operands with UnpackHalf2x16 where f16 denormals are kept
Since #1463, every f16 operand read decodes sign, exponent and mantissa
with integer operations, 8 to 12 instructions more per read than
UnpackHalf2x16 on NVIDIA. The decode works around an AMD Windows driver
that folds the mask of a byte-selected half into UnpackHalf2x16.

A new target flag, float16DenormPreserve, is set when the device reports
shaderDenormPreserveFloat16 and denormBehaviorIndependence ALL, the same
rule as float64DenormPreserve. With it, EmitF16BitsToF32 uses
UnpackHalf2x16 and declares DenormPreserve for 16-bit floats, under
which SPV_KHR_float_controls keeps unpacked subnormal halves. NVIDIA
returns the default NaN for every NaN half, so the NaN bits are rebuilt
from the half: its sign at bit 31, its payload at bits 22:13. The
DenormPreserve 16 mode is declared once, shared with the native f32 to
f16 conversion. Without the flag (lavapipe; RADV reports 32_BIT_ONLY)
the integer decode stays. Halves of fields narrower than 16 bits, such
as SDWA byte selects, keep the integer decode on every device.

The flag is serialized (request version 17) and part of the cache key.

New test agc_driver_f16_to_f32_unpack converts all 65,536 halves, with
nonzero high bits, through v_cvt_f32_f16, and byte-selected halves
through v_add_f16 SDWA, with each decode the device supports, and
compares the bits with an exact decode. agc_driver_f16_to_f32_unpack_spirv
checks which decode the SPIR-V uses with and without the flag.
2026-10-13 12:15:00 +02:00
JAB-dev a96f8bd640 feat(agc): encode packed R11G11B10 float DCC fast clears
A fast clear of a packed float color target (format 36) was refused as unencodable. Give the format
a DCC layout (11, 11 and 10 bit float channels) and its one value, so 0000, 0001, 1110 and 1111
clear codes fill the texels and a texture keeps those keys.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 23:14:10 +13:00
Claude d0df62bd1f
fix(libc): format strftime and asctime as FreeBSD 11 does
strftime handed most conversions to the host's strftime, which differs
from FreeBSD 11's for E and O modifiers on conversions glibc does not
pair with them (%Ea was printed literally), padding flags on %+ and %v,
years outside 1000 to 9999 (%Y printed 899 and -1 where FreeBSD prints
0899 and -001), and out-of-range fields (%I of hour 100, %u of weekday
8). On Windows the padding flags printed nothing. strftime now follows
FreeBSD 11 strftime.c with the C locale on every host, and asctime
follows FreeBSD 11 asctime.c, padding years to four digits and setting
longer years apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb
2026-10-13 10:12:15 +00:00
Mouad-Scriptz-0 df3d027ebe perf(libkernel): find guest allocations and direct mappings by address
sceKernelVirtualQuery, sceKernelMunmap, sceKernelReleaseDirectMemory and the
reservation paths walked a copy of the whole allocation registry or of every
direct mapping under one lock on each call. The registry now answers lookups
from its ordered map, applies range replacements as patches instead of
rebuilding the map, and direct mappings carry an index by physical offset so a
release finds only the mappings it covers. Minecraft (PPSA17221) holds about
7,000 direct mappings and spent its world generation in these walks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 23:11:32 +13:00
Adrià Franch ff6c1ea5f3 fix(stubgen): reconfigure stdout only when it supports it
Callers that replace sys.stdout (test_stubgen redirect_stdout) have no reconfigure().

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 12:07:13 +02:00
GalaxyGamingFR f6d5404f45 fix(stubgen): write LF to stdout on Windows
When --out was not given, stubgen wrote the generated code and debt lines to
sys.stdout, which translates LF to CRLF on Windows, so redirecting stdout
produced CRLF files. stdout is now reconfigured to utf-8 with LF newlines,
matching the --out path. A subprocess test checks the bytes on stdout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq
2026-10-13 12:06:59 +02:00
David-cmrg 9f9c351f54 fix(libSceSystemService): run guest exit handlers first on LoadExec exit 2026-10-13 12:03:05 +02:00
GalaxyGamingFR 0817454371 fix(tools): drop the branch's own pull request in pr_overlap --branch
The --branch mode removed the branch's own open pull request only when its
head equalled the local branch exactly. Checking commits before pushing
left the branch ahead of its pull request, so the tool reported the user's
own pull request as overlapping. The filter is now a function that also
drops any pull request whose head is an ancestor of the branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq
2026-10-13 12:00:41 +02:00
MarcZX100 81f727c051 test(libc): match syslog PID formatting 2026-10-13 09:56:31 +00:00
MarcZX100 7313cf5589 docs(libc): record syslog tag fallback gap 2026-10-13 09:56:31 +00:00
MarcZX100 653c5449f9 fix(libc): honor openlog identity and facility 2026-10-13 09:56:31 +00:00
Alex 988d304c5c fix(libc): keep the acosf result and leave errno unchanged
The previous commit replaced the out-of-range result with the default
quiet NaN 0x7fc00000, but main and FreeBSD e_acosf.c return 0xffc00000.

acosf now calls the host and restores errno, so only errno differs from
main. The guest test pins the result bits for 2.0f and -2.0f and keeps
the in-range case acosf(0.5f).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 11:49:14 +02:00
Alex 64e244eb70 fix(libc): match FreeBSD acosf errno
acosf returned 0xffc00000 (errno 33) for 2.0 (0x40000000), where FreeBSD
lib/msun/src/math.h returns 0xffc00000 (errno 0).

It now returns the FreeBSD result for that case. A guest test covers it
and fails on main.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-13 11:49:14 +02:00
GalaxyGamingFR d15cc41299 fix(conventions): read the PR body file with utf-8-sig
A pull request body saved with a UTF-8 byte order mark starts with
U+FEFF before "### What", so the first heading is not recognised and the
checker reports the What section as missing. Decode the body file with
utf-8-sig so the mark is dropped and the sections are found.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq
2026-10-13 11:48:36 +02:00
GalaxyGamingFR 02dad4429f fix(progress): recognise non-static and noexcept stub helpers
The stub helper pattern required a leading static and a plain signature,
so exports calling helpers in anonymous namespaces, or helpers with
noexcept, a trailing return type or a pointer next to the name, counted
as done. The pattern now anchors at line start, makes static optional,
skips APS5_VABI exports and control keywords, and accepts those suffixes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq
2026-10-13 11:42:32 +02:00
Feuris 572d330a13 feat(libc): implement _Cnd_timedwait 2026-10-13 06:34:01 -03:00
xXPBSXx 51c7206fd0 fix(libScePlayGo): answer chunk ids from the package layout
A package with no playgo-* file in sce_sys has no PlayGo data, so only chunk 0
is valid and any other id returns SCE_PLAYGO_ERROR_BAD_CHUNK_ID. A package with
playgo-scenario.json but no chunk table (PPSA01748) keeps accepting every id.
A chunk table (playgo-chunk.dat or playgo-chunkdefs.xml) still decides when it
exists. A sce_sys that holds other playgo-* files without a chunk table or a
scenario file, or a playgo-chunk.dat that is not a plgx table with a nonzero
count, throws naming the file, since no such dump is known.

Without this a title that walks the ids until the first error (Sifu PPSA03001)
never gets the error.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 11:26:43 +02:00
Rujul Talekar 290675799e test(tools): drop the Ninja-on-PATH requirement from the registration suite
Remove test_built_uses_configured_ninja_outside_path per review: it copied
whatever ninja was on PATH and asserted one existed, so the suite failed on
exactly the setup this PR fixes. The rewritten tests already verify the
configured CMAKE_MAKE_PROGRAM is the program that runs; the suite now passes
with and without Ninja on PATH on Windows and Linux.
2026-10-13 14:51:04 +05:30
Ondra97 bde680c855 fix(relinker): map standalone RELRO in unused import filtering 2026-10-13 11:19:36 +02:00
screamm 5db1d8d5e9 fix(libkernel): accept POLLPRI on guest sockets under WSAPoll
Three catalog titles stop on Windows when they poll a guest socket for
POLLPRI|POLLOUT to see whether a non-blocking connect has completed:

- PPSA97510 PSXS5 (socket 0x10000002, thread gamedb, 13 s):
  what():  poll: POLLPRI is not supported by WSAPoll
- PPSA98510 SwanStation (sockets 0x10000003/0x10000002, threads gamedb
  and update): what():  poll: POLLPRI is not supported by WSAPoll
- PPSA99003 ProsperoTV ({0x10000001, POLLPRI|POLLOUT} at 5.3 s):
  what():  poll: POLLPRI is not supported by WSAPoll

FreeBSD's sopoll_generic (sys/kern/uipc_socket.c, freebsd-src 8c639945)
reports POLLPRI and POLLRDBAND alike, only while out-of-band data is
pending (so_oobmark or SBS_RCVATMARK). WSAPoll rejects POLLPRI in events
with WSAEINVAL but reports pending urgent data as POLLRDBAND, so on
Windows the guest POLLPRI is now asked of WSAPoll as POLLRDBAND and
POLLIN as POLLRDNORM (Winsock's POLLIN includes POLLRDBAND, which would
report urgent data as readable). Measured on this host: WSAPoll reports
POLLRDBAND after a peer's send(MSG_OOB), waits for it, and clears it once
the byte is read with MSG_OOB. Linux keeps polling POLLPRI on the host.

select needs no change: libkernel's select passes exceptfds as Urgent to
the libSceNet poller, which already asks WSAPoll for POLLRDBAND, and it
does not take libkernel guest sockets on either host.

The guest send and recv refuse MSG_OOB, so urgent data only comes from a
peer outside the guest's sockets; guest_poll now sends it from a host
socket and checks that a connected pair and a non-blocking connect
report POLLOUT and not POLLPRI. TechnicalDebt records where Winsock's
answer differs from FreeBSD's once the stream passes the mark.

Depends on #3649 (poll on file descriptors), whose two commits this
branch carries, since the titles reach this stop only with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 11:15:48 +02:00
Sanjaysrinivas 0612c181ff fix(agc): avoid storage snapshot overlap
Create uploads before publishing snapshot validity. Keep allocation
edits separate from the per-unit validity changes in #5024.
2026-10-13 11:11:58 +02:00
eljackjoestar-cmd dedb869fa2 test(agc): shut down the layered submission driver before exit 2026-10-13 13:07:18 +04:00
eljackjoestar-cmd 2ae227ef85 fix(agc): integrate merged position exports and request version 21 2026-10-13 13:03:12 +04:00
eljackjoestar-cmd 78ad5d9dfb fix(agc): validate dynamic layer limits and host subgroup size 2026-10-13 13:01:17 +04:00
eljackjoestar-cmd 13c4a2a6c6 fix(agc): preserve main interfaces in the layered rendering rebase 2026-10-13 13:00:51 +04:00
eljackjoestar-cmd a103e48c8b test(agc): use the current fragment push slot in layered readback 2026-10-13 13:00:28 +04:00
eljackjoestar-cmd 93a090ed8d test(agc): retain array-view checks after merge 2026-10-13 13:00:28 +04:00
eljackjoestar-cmd 60a1649e76 fix(agc): render all attachment layers with dynamic rendering 2026-10-13 13:00:28 +04:00
alfalfa-star 0d61dad293 docs(agc): record the DCC key extent assumed for 3D color targets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:00:28 +04:00
alfalfa-star 425a4daf16 fix(agc): report layer exports as ignored only when a draw does not route them
The warning fired for every draw with PA_CL_VS_OUT_CNTL layer bits,
including layered draws that now route the render target index. It is
checked after the color targets are decoded and leaves out the routed
bits and the misc vector enable. The comment that said layer exports
are never routed is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:00:28 +04:00
alfalfa-star dd70286889 test(agc): draw a layered volume through the driver
agc_driver_layered_volume_submit submits a DCB whose context registers
alone describe a 3D render target (CB_COLOR_ATTRIB3 resource type 3D,
CB_COLOR_VIEW slices 1-3) and set PA_CL_VS_OUT_CNTL USE_VTX_RENDER_
TARGET_INDX, with both shaders registered before the draw. It checks
that each band lands in its own slice. Removing the layer export from
the vertex stage info in Driver::draw, or the bit-18 read in
DecodeState, fails it. Two more runs draw the volume under DCC keys:
uncompressed keys stay uncompressed, and 0000 fast-clear keys leave
every texel the draw does not write at the clear color and end
uncompressed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:00:04 +04:00
alfalfa-star 97d192eeae test(agc): pass the GPU flag to Mutation::Add in the layered volume test
Main added the GPU-visible argument to GuestAllocations::Mutation::Add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:00:04 +04:00
alfalfa-star 1055617751 feat(agc): render layered draws into 3D color targets
A color view of several depth slices of a 3D target (CB_COLOR_VIEW
SLICE_START < SLICE_MAX) was rejected as "color views of several array
slices are unsupported". Titles render such volumes with one draw: an
instanced draw whose vertex stage picks each primitive's slice through
the misc position export (PA_CL_VS_OUT_CNTL USE_VTX_RENDER_TARGET_INDX
with VS_OUT_MISC_VEC_ENA).

Depends on #423, which carries PA_CL_VS_OUT_CNTL to the recompiler.

- DecodeColorBuffer takes the slice range of a 3D target
  (ColorTarget::layers, clamped to the surface's last slice), allows DCC
  on 3D targets, and sizes a tiled 3D target by its whole surface
  (SurfaceForTarget moves to State.cpp for that). DecodeState sets
  State::renderLayers and, for a layered draw, USE_VTX_RENDER_TARGET_INDX
  (State::layerExports); color targets of different slice counts and
  layered draws with a depth target throw.
- The driver adds State::layerExports to the PA_CL_VS_OUT_CNTL bits #423
  passes (it already passes VS_OUT_MISC_VEC_ENA), so the render target
  index reaches gl_Layer only for layered draws and stays ignored
  elsewhere as before.
- A vertex stage writing gl_Layer declares ShaderViewportIndexLayerEXT
  and SPV_EXT_shader_viewport_index_layer when the device offers them
  (the device enables VK_EXT_shader_viewport_index_layer when present),
  instead of SPIR-V 1.5 ShaderLayer, which a Vulkan 1.1 device cannot
  take. Graphics validation accepts the capability, the extension and a
  vertex Layer output when it is enabled, and a per-primitive Layer
  output of a mesh stage.
- The resident target's attachment view is a 2D array view of the
  slices, and the framebuffer has that many layers (Framebuffer and
  AcquireFramebuffer take the layer count).

The execution test agc_driver_layered_volume_draw draws three bands,
one per layer, into slices 1..3 of a 4-slice 64 KiB-tiled volume and
reads every slice back: slice 0 stays untouched and slice 1 + L holds
only band L. It also recompiles a mesh program with the layer export and
validates it against the layered state. The mesh case does not draw:
lavapipe's LLVM backend aborts compiling that mesh pipeline ("Cannot
emit physreg copy instruction"), which a test cannot catch. On RADV the
same mesh draw renders the bands into the right slices.

PPSA14632 (LEGO Horizon Adventures) renders its 64x64x64 RGBA16F
translucency lighting volumes (two DCC targets, 64 instances) this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:00:04 +04:00
Sanjaysrinivas 3f314030b4 fix(agc): use storage factory in snapshot test 2026-10-13 10:59:20 +02:00
Sanjaysrinivas a296b9cacf fix(agc): register snapshot fixture GPU memory 2026-10-13 10:59:20 +02:00
Sanjaysrinivas 65eb466ddd fix(agc): avoid snapshot upload PR conflict 2026-10-13 10:59:20 +02:00
Sanjaysrinivas e2ee9430bf perf(agc): allocate CPU snapshots on demand 2026-10-13 10:59:20 +02:00
Celegans12 23342340cc feat(agc): implement quad tessellation with counter-clockwise output and level clamps
DecodeShaderStages accepted only clockwise triangle patches and no HOS
level clamp narrower than [1, 63], so terrain drawn as TRI_CCW quad
patches with VGT_HOS_MAX_TESS_LEVEL 9 aborted. Quad domains and TRI_CCW
now decode and emit Quads and VertexOrderCcw; isolines, other
partitionings and point or line output still throw.

VGT_HOS_MIN/MAX_TESS_LEVEL are read per draw and pushed in the last 8
bytes of the pre-rasterization push constant slot, where the hull reads
them to clamp its factors; they stay out of every key and the recompile
request, so one hull serves every level. Zero, negative and NaN outer
factors still cull; clamps outside 0 <= min <= 63, max >= 1,
min <= max throw. Quad hulls write four outer and two inner factors at
a 24-byte stride, the layout Mesa's TF ring store uses.
2026-10-13 10:56:16 +02:00
Dean Galvin e5449936f4 fix(agc): keep CPU writes when a pending storage result is stored over untracked memory
On memory outside the write watch a write-back compares guest memory with
the bytes saved at the upload and leaves the 64 KiB blocks that differ to
the CPU. A store of some of an image's units marked the whole saved copy
invalid, and a fill turned into an image clear or an image copy never
saved the bytes under it, so the next store had nothing to compare and
wrote its layers whole over what the CPU had written since.

The saved copy is now valid per tracked unit. A store, a drop or a stale
eviction invalidates only its own units, a CPU-path store makes the units
it stored valid again, and the write-back compares each run of selected
units that still has saved bytes. FillClear and CopyFrom save the bytes
of the units they fill when the surface is not watched, as an upload does.

Results over a range that left the watch while they were pending still
have no saved bytes; storing them now throws instead of overwriting the
CPU's writes (technical debt).
2026-10-13 21:46:12 +13:00
Santhosh Kumar 44623222c9 fix(agc): exclude skipped vertex records from target alias checks 2026-10-13 14:00:11 +05:30
Claude 13444c82e9
fix(libc): print NaN without a sign in the guest printf formatter
The guest formatter handed NaN to the host's snprintf, which prints
-nan for a negative NaN and +nan or " nan" with the + and space flags.
FreeBSD 11's vfprintf clears the sign of every NaN and prints nan (NAN
for the upper-case conversions), keeping the width and the - flag and
ignoring 0. The formatter now does the same for a, e, f and g and
their long double forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb
2026-10-13 08:26:41 +00:00
pushkarreddyy 837c862e22 feat(libkernel): implement sigpending 2026-10-13 13:30:35 +05:30
MarcZX100 7bb59f73b4 docs(libSceLibcInternal): record spawn import stops 2026-10-13 07:56:19 +00:00
David 2fe382c658 feat(libSceLibcInternal): implement the posix_spawn attribute and file action calls
XPSemu (PPSA97358) imports eleven posix_spawn calls from
libSceLibcInternal.prx, one reference each, and no call site is known.
Predicted stop (import order, not measured), behind isnanf (idx 296):
`FAIL: unresolved ELF import 44hlATrd47U from libSceLibcInternal.prx`
(posix_spawnattr_init, idx 347), followed by p--TkNVsXjA, LG6O0oW9bQU,
Sj7y+JO5PcM, Ud8CbISKRGM, Hq9-2AMG+ks, IUfBO5UIZNc, Heh4KJwyoX8 (348-355),
UV4m0bznVtU, Ani6e+T-y6Q and 3Nr9caNHhyg (379-381).

The nine object calls follow FreeBSD release/11.4.0 (8c639945a445)
lib/libc/gen/posix_spawn.c: posix_spawnattr_init allocates a zeroed
attribute object (ENOMEM on failure), _destroy frees it, _setflags and
_setsigdefault store the value; posix_spawn_file_actions_init allocates an
empty list (-1 on failure, as in 11.4), _destroy frees it with its
entries, and _addopen (path copied), _adddup2 and _addclose append in
order after returning EBADF for a negative descriptor. Null or
uninitialized objects, which FreeBSD dereferences, throw.

posix_spawn and posix_spawnp throw "process creation not implemented",
as execvp does: FreeBSD validates no argument before rfork/vfork, and no
child process can be created here. TechnicalDebt records them.
guest_posix_spawn covers the attribute round trips, the recorded action
sequence, the EBADF cases and the throws. NIDs computed with
tools/nid_names.py and present in aerolib.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 07:56:19 +00:00
Kikoarl 396b5189a9 perf(agc): disambiguate completion label overlaps in CopyBuffer 2026-10-13 09:56:09 +02:00
eljackjoestar-cmd 68c2b0f746 fix(agc): prioritize demanded shader preparation 2026-10-13 11:54:29 +04:00
David a7ea2bdcaa feat(libkernel): add mlockall
mlockall (x7g7Ebeo8-U, aerolib) follows FreeBSD 11.4's sys_mlockall
(sys/vm/vm_mmap.c:1067-1131, freebsd-src
8c639945a44588f6cfdddfb057bb5aad88735da0) for the flag check: 0 or
bits outside MCL_CURRENT (1) and MCL_FUTURE (2) fail with EINVAL. The
title process is taken to pass priv_check(PRIV_VM_MLOCK), as mlock
already does.

Valid flags throw. MCL_FUTURE would need every later guest and host
mapping to be locked; nothing in the mapping paths does that, and
passing it to the host would make later host allocations fail above
the host RLIMIT_MEMLOCK. MCL_CURRENT would have to lock the whole
process, which on AnyPS5 also holds host stacks, heaps, modules and
driver mappings (sceKernelVirtualQuery reports them as guest memory),
with ENOMEM above a guest RLIMIT_MEMLOCK that is not known (getrlimit
throws for it). mlock pins host pages (VirtualLock, mlock) range by
range and keeps no record, so there is no state to extend. Recorded
in TechnicalDebt.

XPSemu (PPSA97358) calls it only with -overcommit mem-lock=on
(ZiZc3/XPSemu 8eec07b4, os-posix.c:330-355, reached from
system/vl.c:831-835):

    int flags = MCL_CURRENT | MCL_FUTURE;
    ...
    ret = mlockall(flags);
    if (ret < 0) {
        error_report("mlockall: %s", strerror(errno));
    }

and exits on failure, so that option ends in the throw; without it
the title is not affected. Predicted stop removed (registry index 201,
not run):
FAIL: unresolved ELF import x7g7Ebeo8-U from libkernel.prx

guest_mlock checks EINVAL for 0, 4, 5 and -1 and the throw for 1, 2
and 3.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 87f4aa331b02d098fe27516809495447ff6a9fa6)
2026-10-13 07:51:35 +00:00
Arekatla Nishanth Chowdary ebd71eb2e7 test(libSceRudp): follow the End/Terminate swap in the context query and pump tests
guest_rudp_context_queries released its context with sceRudpEnd(context)
and shut the library down with sceRudpTerminate(), and guest_rudp_pump
shut it down with sceRudpTerminate(), the order main had before the
swap. They now call sceRudpTerminate(context) and sceRudpEnd().
2026-10-13 07:51:20 +00:00
David 4029b5d2c9 fix(libSceRudp): swap sceRudpEnd and sceRudpTerminate
sceRudpEnd takes no argument and shuts the library down; sceRudpTerminate
takes a context id and releases that context. main had the two swapped,
so a title calling sceRudpEnd() passed a stray register as a context id
and the library stayed initialised.

Evidence:
- PS5 console measurement by hugodearaujo in #3073 (firmware 13.60):
  "After `sceRudpEnd()` all three return `0x80770001` again"
  (sceRudpGetMaxSegmentSize, sceRudpProcessEvents, sceRudpNetFlush).
- AlexAltea/orbital cc408367da6a6ebbf5ac1ac559fefeadc73b2831
  tools/ida/db_types.json:1971,1996: `int sceRudpEnd(void)`,
  `int sceRudpTerminate(int ctxId)`.
- RPCS3 26098fb42d090cd522b4afcf3b754dd987c82978
  rpcs3/Emu/Cell/Modules/cellRudp.cpp:108: `cellRudpEnd()` tears the
  library down.
- AndnixSH/Vita3K-PS-Vita-OS-Fork c86ac732bc811057e690bed280c5076bdf95385e
  vita3k/modules/SceRudp/SceLibRudp.cpp:1032,1614: `sceRudpEnd()` clears
  every context, `sceRudpTerminate(int ctx_id)` erases one.

The bodies are unchanged, only exchanged. guest_offline_imports now ends
the library with sceRudpEnd(), and guest_rudp_status checks that
sceRudpTerminate releases one context and sceRudpEnd all of them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 07:51:20 +00:00
MarcZX100 a4752266b8 docs(libc): record Windows directory fopen behavior 2026-10-13 07:51:09 +00:00
MarcZX100 eefe4db018 fix(libc): return native fopen errors 2026-10-13 07:51:09 +00:00
Blazing Cook acd8c8c527 test(ampr): retain both pooled-page ownership regressions
Restore the original alias-remap regression alongside the pool-exhaustion
regression so each ownership fix remains covered after combining the series.
2026-10-13 07:50:53 +00:00
MarcZX100 45a157d931 fix(ampr): reserve remap targets before releasing pool pages 2026-10-13 07:50:53 +00:00
Blazing Cook a88a4c8e15 fix(ampr): reserve targets before releasing mapped pool pages
(cherry picked from commit 3e9a37eaa34c4d543f775bece42de3eb7a4647ea)
2026-10-13 07:50:53 +00:00
Blazing Cook ce98f83033 fix(ampr): retain pooled pages while remapping aliases
(cherry picked from commit a2408b5244c663849ae4f7022e7aff01f2ba5abc)
2026-10-13 07:50:53 +00:00
MarcZX100 3e238ff11d fix(shader): accept MIMG sampler tuples ending in VCC 2026-10-13 07:40:17 +00:00
MarcZX100 8d455b8018 docs(libkernel): qualify atfork title reachability 2026-10-13 07:39:26 +00:00
David e8fd542f03 feat(libkernel): implement pthread_atfork
pthread_atfork (U9t5kJAWPnA, aerolib) appends the prepare, parent and
child handlers to a process-wide list under a lock and returns 0, or
ENOMEM when the entry cannot be allocated, as FreeBSD 11.4's
_pthread_atfork does (lib/libthr/thread/thr_fork.c:78-100, freebsd-src
8c639945a44588f6cfdddfb057bb5aad88735da0). Null handlers are accepted;
FreeBSD's fork skips them. The handlers never run: fork
(libkernel_sys) throws and vfork is absent, so no child process is
created. The list is kept, in registration order, for a later fork
(PthreadAtforkHandlers_nid_no_patch); recorded in TechnicalDebt.

XPSemu (PPSA97358) calls it from a constructor, before main
(ZiZc3/XPSemu 8eec07b4, util/rcu.c:488-495):

    static void __attribute__((__constructor__)) rcu_init(void)
    {
        smp_mb_global_init();
    #ifdef CONFIG_POSIX
        pthread_atfork(rcu_init_lock, rcu_init_unlock, rcu_init_child);
    #endif
        rcu_init_complete();
    }

so a throwing export would stop the title before main. Predicted stop
removed (registry index 255, not run):
FAIL: unresolved ELF import U9t5kJAWPnA from libkernel.prx

guest_pthread_atfork checks registration order, null handlers,
concurrent registration from four threads and that no handler runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit f37bccf6efd76edcde5d582800c89b83fdd30472)
2026-10-13 07:38:13 +00:00
ideabsassistenza 6fb0727c48 fix(debt): preserve leading entries during conflict resolution 2026-10-13 09:34:22 +02:00
Yunus Shaikh 60fb528572 refactor(libSceNet): place the SCE timeout constants and timeout_option apart from new neighbours
#4988 adds a constant after NET_SO_RCVTIMEO and #4989 adds a function before receive_error, so defining the SCE timeout constants and timeout_option there made the pull requests conflict.
2026-10-13 00:34:06 -07:00
Ondra97 74e239408b fix(tools): reject mixed high refresh skip diagnostics 2026-10-13 09:25:12 +02:00
Ondra97 7ebad36341 fix(tools): accept skips on displays without 120 Hz 2026-10-13 09:19:28 +02:00
babyhuey 0daf1d5ba3 fix(libSceNet): report SO_ERROR from sceNetGetsockopt 2026-10-13 03:15:45 -04:00
Ondra97 6706e44d95 fix(conventions): ignore C++ raw literal contents 2026-10-13 09:15:31 +02:00
Doston Abdullayev 9cdcd58171 fix(libkernel): use precise waits for empty poll sets
Carry the poll-only portion of Doston Abdullayev’s public #1471 fix, preserving its original authorship. The select timeout change is already covered separately by #4754.
2026-10-13 07:11:08 +00:00
Alex Collini a141b8eb67 fix(agc): drop the comment in SrgbTwinFormat
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:09:33 +02:00
TideOPL 007721d43e perf(shader): index specialization tables by SPIR-V id
SpecializeSpirv kept its per-id data (folded values, result types, scalar
types, removed ids, blocks, edges, copies, definitions and the live sets) in
std::map and std::set, and repeats fold, prune and copy propagation over the
whole module until nothing changes. Ids are dense below the header's bound,
so the tables become vectors indexed by id. Lookups of operands that are
literals rather than ids stay bounds-checked, iteration keeps ascending id
order, and an id at or beyond the bound now throws.
2026-10-13 07:59:08 +01:00
Alex Collini 12d07b8edd fix(agc): give R8 and R8G8 no sRGB twin where the device decodes them in the shader
SrgbTwinFormat mapped R8_UNORM and R8G8_UNORM to R8_SRGB and R8G8_SRGB
without asking the device. Where 8_SRGB or 8_8_SRGB is decoded in the
shader (context.srgbDecodeFormats: the device cannot sample the sRGB
format, or APS5_SRGB_SHADER_DECODE=1), the forced view would have been
created with a format the device cannot sample. Those two formats now
get no twin there, so a FORCE_DEGAMMA sampler on them keeps the refusal.
The image is no longer created MUTABLE_FORMAT either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:58:19 +02:00
hypermist 6bf9ab86ff perf(agc): keep adjacent ranges one import serves as one region of the cached address space
A V# spanning two registered ranges dissolved the shared space into the
build's own regions (about 10k regions copied, sorted and looked up per
build in Killing Floor 3), although the import covering the run binds it
in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uzy15wqYTt6iWyxYXEzUWo
2026-10-13 19:03:20 +13:00
MarcZX100 c7aca86c61 fix(tools): use the shared merge base for overlap checks 2026-10-13 06:01:10 +00:00
Ondra97 66edc7463f fix(tools): audit stubs from shared library sources 2026-10-13 07:59:10 +02:00
MarcZX100 2a91815210 fix(tools): parse paginated overlap API responses 2026-10-13 05:52:46 +00:00
QRMOE 2c23ced70e
test(libc): cover llroundl boundaries in every rounding mode 2026-10-13 07:47:45 +02:00
QRMOE f745a00a5d
fix(libc): accept the full llroundl integer range 2026-10-13 07:46:49 +02:00
LeandroLP23 fa889c063b docs(contributing): ask GPU changes to name the GPUs their tests ran on
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:45:50 +02:00
MarcZX100 abc997b0da fix(agc): release cached address space on device teardown 2026-10-13 05:35:37 +00:00
babyhuey d39cb2c62f feat(libkernel): report machdep.tsc_freq from sysctlbyname 2026-10-13 01:18:43 -04:00
MarcZX100 c1bcd0b697 fix(videoout): dump linear display buffers 2026-10-13 05:05:31 +00:00
David 097bc538ee fix(libkernel): fail ftruncate on a pipe, a directory and a device with EINVAL on Windows
(cherry picked from commit 971fb51ef3f09d6543ef20e05e4a44572d2aadbd)
2026-10-13 04:18:07 +00:00
David 2f1eb100a0 fix(libkernel): fail reads on a directory descriptor with EISDIR on Windows
(cherry picked from commit d7ef271ef7eeb1d82a337a0b0a9767ce68721bd4)
2026-10-13 04:00:38 +00:00
milescoviello 7cba4ce60a fix(libkernel_sys): let link follow a symbolic link like FreeBSD
link threw "a symbolic link as the existing path is not supported" when
its first argument named a symbolic link, so a valid call aborted the
title. FreeBSD's link follows that link (sys/kern/vfs_syscalls.c,
sys_link: "kern_linkat(td, AT_FDCWD, AT_FDCWD, uap->path, uap->link,
UIO_USERSPACE, FOLLOW)") and hard-links the file it points to; a link to
a directory then fails with EPERM ("if (vp->v_type == VDIR) ... return
(EPERM);") and a dangling one with ENOENT from namei.

link now checks the followed status of the existing path and, when it
is a symbolic link, creates the hard link to the file it resolves to.
std::filesystem::create_hard_link would otherwise link the symbolic
link itself on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a521101b77c879234308711c5ec8a9f9cf00816f)
2026-10-13 03:46:17 +00:00
Yunus Shaikh d6698c9045 refactor(agc): declare maxDrawSnapshotBytes after guestMemory
#757 and #758 add a member after usesFaultBuffer, so declaring maxDrawSnapshotBytes there made the pull requests conflict.
2026-10-12 19:43:34 -07:00
David 69b9d186db fix(libc): replace an open or read-only destination in rename on Windows 2026-10-13 05:58:01 +04:00
Yunus Shaikh 4ccf888291 fix(savedata): leave the mount result untouched when sceSaveDataMount3 fails
Animal Well (PPSA22520) checks for its save with a CREATE2 mount it never
unmounts, then saves with a second mount of the same directory. That
mount returns BUSY, and the title keeps using the first mount point for
sceSaveDataPrepare, its save file and sceSaveDataUmount2.
sceSaveDataMount3 cleared the result before its checks, so the save was
written to "/save" in the game folder instead of the save directory, and
the first mount was never released. The result is now written only when
the mount succeeds, as shadPS4 does.
2026-10-12 17:47:46 -07:00
David 665e6f1782 fix(libkernel): fill every buffer of readv from a pipe on Windows 2026-10-13 00:30:45 +00:00
TideOPL 126a1e4137 perf(agc): copy a pending block-compressed surface into its sampled texture on the GPU
A compute pass can write a block-compressed surface through an uncompressed
storage view whose element is one block (32_32_UINT for 8-byte blocks,
32_32_32_32_UINT for 16-byte ones). cachedTexture rejected such a storage
image for the BC descriptor, so every sample stored the whole surface to
guest memory, compared it with the snapshot and uploaded it again.

Texture::CanBlockCopyFrom accepts a storage image over the same memory,
tiling and swizzle whose element is the block and whose extent is the
surface's extent in blocks, with whole blocks on every copied level. The
sampled texture then owns a BC image that vkCmdCopyImage refreshes from the
storage image when its version moved, through the same refreshed-on-reuse
path as the depth copy.
2026-10-13 01:29:21 +01:00
babyhuey c15e9f2a37 fix(libSceVideoOut): return RESOURCE_BUSY when a bus is opened twice 2026-10-12 20:26:26 -04:00
Cesar Rodriguez baaad78e81 fix(agc): leave blending off for a color target with CB_COLOR_INFO.BLEND_BYPASS
DecodeState threw "blend bypass conflicts with enabled blending" when
CB_BLENDn_CONTROL enabled blending for a target whose CB_COLORn_INFO has
BLEND_BYPASS (bit 16, Mesa src/amd/registers/gfx10.json) set. The bit makes
the color block skip the blender for that target: Mesa sets it "according
to docs" for UINT/SINT and 8_24/24_8/X24_8_32_FLOAT targets, which the
blender cannot process (src/amd/common/ac_descriptors.c, shared by radeonsi
and RADV). Marvel's Guardians of the Galaxy (PPSA01748) issues such draws
right after its loading screen. Blending is now enabled only when it is
requested and not bypassed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:22:38 -07:00
Simon 51c043ba51 fix(agc): keep the texels of CMASK tiles with codes other than 0 and 15
A fast-clear materialization threw when a CMASK nibble held a code other
than 0 (cleared) or 15 (expanded). PPSA18391 (engine: Clickteam Fusion) fast
clears a target whose CMASK it never initialized, so some nibbles hold
leftover values such as 1 and 10, and presentation failed on the first
frame.

Such a tile now keeps its texels like an expanded tile, on the CPU path
and in the compute pass. A cleared tile of a DCC target still throws.
The graphics and CMASK fast-clear tests now check that an unknown code
leaves the tile unchanged instead of expecting the throw.
2026-10-13 02:17:33 +02:00
Yunus Shaikh 584c59e5ac test(libkernel): compare the unterminated readv buffer with memcmp 2026-10-12 16:37:56 -07:00
john.maia 0900586191 feat(agc): skip the draws and dispatches of one shader with APS5_SKIP_SHADER
APS5_SKIP_SHADER=<code address, hex> drops every draw that runs a program
at that address and every dispatch of a compute program at that address, as
if the packet had run. Skipping one pass at a time shows which pass first
writes a wrong value into an image: Epic Mickey's (PPSA15657) green sky
light was bisected this way to the copy into its sky cube (#4551).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:35:38 +01:00
ArjenS-LA 819c475266 fix(tools): match explicit trace summaries to their capture 2026-10-12 15:18:16 -07:00
Yunus Shaikh 92e0970d6c test(libSceNet): run CheckUnboundSocketName before CheckUnspecifiedIpv6
#4785 calls its netstat check right after CheckPoolStats(), so calling CheckUnboundSocketName() there made the two pull requests conflict.
2026-10-12 15:11:43 -07:00
Yunus Shaikh a7e320d1f7 test(libSceNet): define CheckEpollWritable after CheckMessages
#4785 adds its netstat checks before CheckPoolStats, so defining CheckEpollWritable there made the two pull requests conflict.
2026-10-12 15:09:09 -07:00
Niko 7f39ac991b fix(conventions): keep #### subsections inside the pull request template sections
SECTION matched every heading that starts with ###, so a #### line opened
a new section named "# ...". A Tested section written as #### Linux and
#### Windows subsections was reported as missing or empty, and an
unchecked item after a #### line in Checklist was not reported.

A level-4 heading belongs to the level-3 section above it (CommonMark
4.2), and silent_stub_debt() already keeps deeper headings inside their
section. SECTION now matches only headings with exactly three #.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:14:49 +02:00
ArjenS-LA d31e283088 fix(libkernel): throw for unsupported directory entry names 2026-10-12 21:14:01 +00:00
PrintOrb 5031432db0 test(audioout): detect overlapping backend initialization 2026-10-12 16:04:06 -05:00
PrintOrb c35572ec18 fix(audioout): serialize shared SDL audio initialization 2026-10-12 15:54:46 -05:00
Ondra97 85cf9c3ac7 fix(tools): preserve technical debt section moves 2026-10-12 21:37:58 +02:00
Karan Vora 1dc99ba0a2 perf(shader): drop the input flush that rcp, rsq, sqrt, exp and log results never use
The SPIR-V emitters of FPRecip32, FPRecipSqrt32, FPSqrt, FPExp2 and FPLog2
flushed a denormal input to its signed zero before the host operation.
Since #537, unaryFloatSpecials replaces the host result for every input
below 2^-126 with the hardware's result for a zero of the same sign: rcp
and rsq give a signed infinity, sqrt the signed zero, log -inf and exp 1.0.
floatUnary and float16Unary both go through it, so the flushed input could
only change a value that is then discarded. v_rcp_iflag_f32 gets the same
special values and already went without the flush.

The pixel prologue computes PositionW as FPRecip32 of FragCoord.w without
those special values, so it now flushes FragCoord.w in the IR first and
gives the same result as before.
2026-10-12 15:17:41 -04:00
Yunus Shaikh d0cef9afdb test(libSceNet): define and run CheckEpollWritable before CheckPoolStats
#4673 adds a test after CheckEmptyEpoll and its call after CheckEmptyEpoll(), so placing the new check there made the two pull requests conflict.
2026-10-12 11:57:27 -07:00
Yunus Shaikh 78d594d9f6 Merge remote-tracking branch 'origin/main' into fix/libscenet-epoll-out 2026-10-12 11:56:48 -07:00
Yunus Shaikh ac8106bda2 refactor(libSceNet): define timeout_option before receive_error
#4673 adds a function after receive_error, so defining timeout_option there made the two pull requests conflict.
2026-10-12 11:52:49 -07:00
MarcZX100 acd821f7ce fix(tools): audit sectionless guest modules 2026-10-12 18:46:25 +00:00
Leandro Soares 6054923cd5 fix(tools): audit the imports of --modules files
(cherry picked from commit 2d0d707802c190f4d8ed93d01427576accbc08b1)
2026-10-12 18:46:25 +00:00
Yunus Shaikh 2c0d8d48bc Merge remote-tracking branch 'origin/main' into fix/libc-strtod-quiet-nan-windows
# Conflicts:
#	docs/dev/TechnicalDebt.md
2026-10-12 11:45:40 -07:00
David 61fbec8ef1 fix(libkernel): fail lseek on a pipe with ESPIPE on Windows 2026-10-12 18:33:31 +00:00
Yunus Shaikh 0a1dff2fcc perf(agc): read the draw snapshot cap when resources are built
ShaderResources::PrepareDrawBindings called MaxDrawSnapshotBytes() on
every draw, and it read APS5_DRAW_SNAPSHOT_MAX_MIB with getenv each
time. The UCRT's getenv takes the environment lock and scans the whole
block. In Planet of Lana (about 90,000 draws per 10 s) that call was
2.6% of the queue-0 worker's samples.

The cap is now read once when a ShaderResources object is built and
kept in it, so a cached resource template uses it for every draw it
serves. In Planet of Lana the record phase drops from 21.2 to 18.7 us
per draw.

drawSnapshotCapTests also raises the variable after building the
resources and checks that they keep the cap they were built with. With
the previous ShaderResources.cpp it fails there.
2026-10-12 11:26:09 -07:00
Yunus Shaikh c55f377fb0 refactor(libc): place the NaN helpers and test includes apart from neighbouring changes 2026-10-12 11:03:36 -07:00
MiniRML 63022e23cc Merge remote-tracking branch 'upstream/main' into fix/nid-windows-unicode-paths 2026-10-12 10:05:59 -07:00
MiniRML a7799c4315 Merge remote-tracking branch 'upstream/main' into fix/nid-buffer-validation 2026-10-12 10:05:51 -07:00
QRMOE 79d88af192
test(libc): cover wide transform output bounds 2026-10-12 19:04:00 +02:00
QRMOE 22d03d497f
fix(libc): terminate truncated wcsxfrm results 2026-10-12 19:03:18 +02:00
QRMOE 0950b19854
test(libc): cover exhausted tokenizer state 2026-10-12 19:01:27 +02:00
MiniRML ad43a486f0 test(nid): keep reference registration independent of other topics 2026-10-12 10:00:50 -07:00
QRMOE 3cb434715a
fix(libc): clear exhausted strtok continuation state 2026-10-12 19:00:50 +02:00
MiniRML 1c500fb298 test(nid): separate export-reference registration from path tests 2026-10-12 09:59:52 -07:00
MiniRML 4df7877334 test(nid): avoid competing hash test registration 2026-10-12 09:57:43 -07:00
MiniRML 89d89ac048 fix(nid): preserve each ELF string consumer independently 2026-10-12 09:53:17 -07:00
MiniRML f25eba673d fix(nid): bound version requirement chains to their section 2026-10-12 09:51:20 -07:00
Borooooo 39bed4b9d7 fix(relinker): accept paths longer than 260 characters on Windows
relinker.exe carried only MinGW's default manifest, so Windows kept MAX_PATH for it even with long paths enabled, and an input under a deep directory failed with 'Cannot open file'. Embed a manifest with longPathAware that keeps asInvoker and the Windows 10 supportedOS entry.
2026-10-12 18:50:50 +02:00
MiniRML 617e7acfa8 test(nid): avoid competing hash test registration 2026-10-12 09:46:59 -07:00
MiniRML abe8fb8193 fix(nid): validate ELF table layouts before patching 2026-10-12 09:45:30 -07:00
MiniRML bb443667ec fix(nid): validate GNU hash geometry before rewriting 2026-10-12 09:39:28 -07:00
MiniRML 6e0f942f0f fix(nid): resolve PE reference exports through their ordinals 2026-10-12 09:36:42 -07:00
Rujul Talekar 0c52b2a0b0 fix(tools): use configured CMAKE_MAKE_PROGRAM for test registration audit
tools/check_test_registration.py invoked bare ninja from PATH, so the audit failed with FileNotFoundError when the configured Ninja lived outside PATH. Read CMAKE_MAKE_PROGRAM from the build directory CMakeCache.txt and fail honestly when it is missing. Adapt the mocked built tests to a configured build and add a regression that runs a real Ninja from a path with spaces outside PATH.
2026-10-12 21:59:53 +05:30
MiniRML db577b4a34 fix(release): preflight asset destination types 2026-10-12 09:29:03 -07:00
MiniRML 17a3d8a1a0 fix(oracle): honor current target override 2026-10-12 09:27:33 -07:00
MiniRML 521cc95b18 fix(oracle): validate input words before tool lookup 2026-10-12 09:26:05 -07:00
MiniRML 53d10eac9a fix(tools): exclude cut helpers from NID accounting 2026-10-12 09:24:13 -07:00
MiniRML a6f42f5119 fix(relinker): zero the unmapped section-name table address 2026-10-12 09:10:47 -07:00
MiniRML 7dfd790d08 fix(release): find the configured Windows runtime 2026-10-12 09:05:52 -07:00
Yunus Shaikh aa4a9b4faf fix(libc): return the default quiet NaN from the strtod family
C17 7.22.1.3 and 7.29.4.1.1 interpret NAN(n-char-sequence) as a quiet
NaN. With MinGW-w64, std::strtod, strtof and strtold are mingw-w64's
gdtoa functions, which copy the hex digits into the significand
without the quiet bit, and for long double without the x87 integer
bit: strtod("nan(0x123)") returned the signaling NaN
0x7ff0000000000123 and strtold a pseudo-NaN. A payload whose only set
bit is the implicit or x87 integer bit gave infinity:
strtof("nan(0x800000)"), strtod("nan(0x10000000000000)") and
strtold("nan(0x8000000000000000)") returned +inf. glibc returns quiet
NaNs that keep the payload.

C17 7.12.11.2p2 makes nan("n-char-sequence") equivalent to
strtod("NAN(n-char-sequence)", NULL), and nan and nanf already return
the console's fixed quiet NaNs, ignoring the tag. ParseFloat, which
strtod, strtof, strtold, atof and the wide variants share, now returns
the same values, 0x7ff8000000000000 and 0x7fc00000, negated after a
minus sign, on Windows and Linux. strtold returns the x87 default
quiet NaN, since no console nanl was observed.
2026-10-12 09:04:31 -07:00
Yunus Shaikh 4f78bd534f fix(libSceNet): report copied socket address lengths
sceNetAccept, sceNetGetsockname, sceNetGetpeername, sceNetRecvfrom and
sceNetRecvmsg stored the full address length (16 or 28) even when the
caller's buffer was shorter and only part of the address was copied.
FreeBSD 11.4 stores the copied length, the smaller of the two
(kern_accept4, kern_recvit, kern_getsockname and kern_getpeername in
sys/kern/uipc_syscalls.c), as libkernel's socket calls do since #4295.
2026-10-12 09:00:04 -07:00
Yunus Shaikh feffc67acb fix(libSceNet): name an unbound socket with the zero address on Windows
sceNetGetsockname on a socket that is neither bound nor connected failed
with EINVAL on Windows, where Winsock reports WSAEINVAL. FreeBSD 11.4
returns the wildcard address with port 0 (in_getsockaddr and
in6_getsockaddr in sys/netinet/in_pcb.c and sys/netinet6/in6_pcb.c), and
Linux already does, so the call now builds that address for the socket's
family.
2026-10-12 09:00:04 -07:00
Yunus Shaikh 53cbdebe82 fix(libSceNet): report the registered socket as the epoll event ident
sceNetEpollWait copied the whole event a title passed to
sceNetEpollControl, so the ident and reserved fields came back as the
title had left them. The ident of a returned event is the id that was
registered and reserved is 0 (shadPS4 net.cpp sceNetEpollWaitImpl,
KytyPS5 network.cpp EpollWait); a title that registers a socket without
filling ident and looks the socket up by it now finds it.
2026-10-12 09:00:03 -07:00
Yunus Shaikh b19a74f5bf fix(libSceNet): sceNetEpollWait takes 2 as the OUT event
sceNetEpollWait used Linux's EPOLLOUT bit (4) for writability. The SCE
value is 2 (shadPS4 net_types.h ORBIS_NET_EPOLLOUT, KytyPS5 network.cpp
EPOLL_OUT, SharpProspero Socket.cs EpollOut); IN (1), ERR (8) and HUP
(0x10) already match. A socket registered for OUT was never polled for
writability, so the wait ran out its timeout or blocked forever, and an
IN|OUT registration never reported OUT.
2026-10-12 09:00:02 -07:00
Yunus Shaikh febc36223b fix(libSceNet): take 0x1105 and 0x1106 as the send and receive timeouts
sceNetSetsockopt and sceNetGetsockopt answered EOPNOTSUPP for 0x1105 and
0x1106, the SCE send and receive timeouts in microseconds (shadPS4
net_types.h ORBIS_NET_SO_SNDTIMEO/RCVTIMEO, SharpProspero Socket.cs
SoSndTimeo/SoRcvTimeo, KytyPS5 network.cpp). A title that set a receive
timeout this way then blocked in sceNetRecv for good. Both options now
set and read the timeouts that 0x1005 and 0x1006 already do.
2026-10-12 09:00:02 -07:00
Mohamed Aziz Hadjkacem 4d37c93364 fix(agc,shader): prime the two host shared mutexes added after 48dcaace7
Commit 48dcaace7 introduced PrimedSharedMutex and converted every host std::shared_mutex that existed at the time, because winpthreads initializes a static rwlock on first use and a concurrent first lock returns EINVAL, which std::shared_mutex ignores (docs/dev/TechnicalDebt.md:10).

Two host mutexes were added afterward and missed the conversion:
- Recompiler.cpp registrationModule() static mutex (added by baf3437465)
- MultisampleTarget.cpp guardedMutex() static mutex (added by d109f3966f)

Apply the same priming. Recompiler.cpp reuses the PrimedSharedMutex already defined in that translation unit; MultisampleTarget.cpp gets the identical local helper to match the existing inline precedent.

Tested: Windows 11 x64, WinLibs GCC 15.2.0 (matches CI). Compiled Recompiler.cpp.obj and all three MultisampleTarget.cpp.obj targets with the project build flags; check_conventions.py passes.
2026-10-12 16:52:00 +01:00
MiniRML bf6a054c52 fix(nid): preserve Unicode paths on Windows 2026-10-12 08:43:02 -07:00
Xabilimon1 aa88949e2e fix(libkernel): build the fcntl status flags and the getrlimit test on macOS
InitialEntry reads O_DIRECT from the host status flags, and macOS has
no O_DIRECT, so libkernel did not compile there. It is now read only
where the host defines it. GuestGetrlimit iterates over braced lists
without including <initializer_list>, which libc++ requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:33:37 +02:00
MiniRML ca67860168 fix(nid): validate buffer ranges and string terminators 2026-10-12 08:28:52 -07:00
thuanquang 4feeef6981 test(tools): create hard links with os.link for Python 3.9
Path.hardlink_to was added in Python 3.10, so import_audit and stubgen
errored on Python 3.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:05:04 +07:00
Karan Vora 69d0dc2288 perf(shader): treat f32 min, max, med3, floor and ceil results as denormal-free
In the float modes that flush f32 denormals, writeOperand flushes the result
of v_min/max/min3/max3/med3_f32 and v_floor/ceil_f32. DenormalFlushEliminator
drops a flush only when it can show the value isn't a denormal, and it had
no rule for these opcodes, so their result flush stayed even when every
operand had already been flushed.

FPMin32, FPMax32, FPMinTri32, FPMaxTri32 and FPMedTri32 return one of their
operands' bits, or the and or or of two zeros, so the result is denormal-free
when every operand is. FPFloor32 and FPCeil32 return an integer, a zero, an
infinity or a NaN whatever their operand. In IEEE mode the min and max
results and the floor and ceil results go through selects with a quieted
operand, which the eliminator can't follow, so their flush stays there.

agc_denormal_flush_elimination covers the five min and max forms with
flushed and with unknown operands, floor and ceil of an unknown value, and a
max carried around a loop.
2026-10-12 09:24:29 -04:00
Alex Collini ff3eae6c9a feat(agc): sample non-sRGB textures through FORCE_DEGAMMA samplers as sRGB views
A sampler with FORCE_DEGAMMA decodes the texels of a non-sRGB 8-bit texture
as sRGB. Such a binding was refused; it now samples an sRGB view of the same
image. Snapshot images whose format has an sRGB twin (R8, R8G8, RGBA8, BGRA8,
ABGR8 packed, BC1/2/3/7 UNORM) are created with MUTABLE_FORMAT, and storage
images are already mutable. The view is made on first use for the full,
first-layer or base-level range, with sampled usage only and the descriptor's
minimum LOD. A format without an sRGB twin is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:11:34 +02:00
marcio-ch 3bc3e35a02 fix(agc): handle the fill kernel that reads its pattern from a buffer
The 16-byte fill kernel has a variant that loads its pattern with
s_buffer_load_dwordx4 through the V# in s[4:7] instead of taking it from
the user SGPRs. Vampire Crawlers (PPSA30703) clears its UI canvas HTILE
with it every frame; run as a shader, the depth surface never cleared and
every UI draw failed its GEQUAL depth test, leaving the screen black.
2026-10-12 09:32:17 -03:00
danzaio b390831a58 docs(user): relocate Windows standard handles logging note to Windows runtime layout section 2026-10-12 00:18:47 -03:00
danzaio ae96779914 test(relinker): verify Windows DLL CRT output handles 2026-10-12 00:15:49 -03:00
ChatGPT (local) 6effc5dcec test(relinker): verify GUI startup log replacement and unavailable log
Cover successful GUI stdout logging, replacement of stale log, and startup when log path is a directory. AI-assisted: ChatGPT.
2026-10-12 00:15:49 -03:00
Doston Abdullayev ab3e926058 feat(relinker): write the output of a Windows executable without standard handles to anyps5.log
A --windows-gui build started from Explorer, or any Windows executable
started without a standard output or error handle, lost everything it
printed: the entry stub's messages, the libraries' stdout and stderr,
uncaught exception messages and crash reports.

Before it enters its directory or loads a library, the entry stub now
checks the standard output and error handles, and gives each missing one
anyps5.log beside the executable. The C runtime of the libraries it then
loads adopts the file. Executables started with handles are unchanged,
and one that cannot create the file starts as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:15:49 -03:00
Karan Vora d759a43ae5 perf(shader): search a bindless image table once per access
EmitImage selects the table slot of a bindless image before it branches over
the image's runtime modes, and EmitImageMode then searched the table again in
every mode arm. Each search is a binary search of dependent flattened_srt
loads, so every bindless sample ran two. The mode arms now take the selection
EmitImage already made; the modes copy the base image, so the mapping offset,
root and iteration count the search reads are the same.
2026-10-11 21:22:52 -04:00
nyavana 67cdc9edcc docs(relinker): correct help and exit-code usage
Document --help output and status, distinguish no-argument errors, and include help in the successful exit code description.

AI-assisted: yes (OpenAI Codex).
2026-10-11 16:09:56 -07:00
498 changed files with 23507 additions and 2033 deletions

View File

@ -14,4 +14,5 @@
- [ ] Generic behaviour, not specific to one title
- [ ] Depends on: <!-- #PR, or none -->
- [ ] Unblocks: <!-- title ID and Game bring-up discussion, e.g. PPSA01325 #3232, or none -->
- [ ] GPUs tested: <!-- for AGC, shader recompiler or VideoOut changes: vendor, model, driver and OS of each GPU the GPU tests ran on; otherwise n/a -->
- [ ] AI-assisted: <!-- yes / no -->

View File

@ -12,6 +12,11 @@ on:
description: Pull request number to build
required: true
type: string
linux:
description: Also build and test on Linux
required: false
default: true
type: boolean
permissions:
contents: read
@ -103,7 +108,7 @@ jobs:
}
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
const touches = (...prefixes) => manual || files.length >= 3000 ||
const touches = (...prefixes) => files.length >= 3000 ||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
const workflow = '.github/workflows/build.yml';
const libc = 'core/libs/prx/libc/';
@ -214,7 +219,7 @@ jobs:
linux:
needs: eligibility
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@ -241,6 +246,15 @@ jobs:
evict-old-files: job
- name: Install lavapipe and the SDL audio and XRandR development files
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev
- id: mesa
run: |
echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT"
echo "cpu=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@v6
with:
path: .mesa-shader-cache
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
restore-keys: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
- name: Check that every test executable is registered with ctest
run: python3 tools/check_test_registration.py build
@ -250,6 +264,12 @@ jobs:
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
MESA_SHADER_CACHE_DIR: ${{ github.workspace }}/.mesa-shader-cache
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !cancelled() }}
uses: actions/cache/save@v6
with:
path: .mesa-shader-cache
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
- run: python3 tools/check_skips.py build/ctest.xml
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
@ -304,7 +324,7 @@ jobs:
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
evict-old-files: job
- shell: cmd
run: cmake -S . -B build %CONFIGURE%
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
- name: Check that every test executable is registered with ctest
shell: cmd
run: python tools/check_test_registration.py build

View File

@ -68,6 +68,14 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler)
add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests)
set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20)
add_test_executable(request_serializer_tests
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp)
target_include_directories(request_serializer_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include)
add_test(NAME request_serializer COMMAND request_serializer_tests)
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
@ -84,6 +92,10 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py)
add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py)
add_test(NAME progress_wrappers COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_wrappers.py)
add_test(NAME progress_read_encoding COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py)
add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE}
@ -100,12 +112,16 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_debt_split.py)
add_test(NAME stubgen_stdout_newlines COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py)
add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
@ -115,6 +131,10 @@ if(BUILD_TESTING)
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py)
add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py)
endif()
endif()

View File

@ -60,6 +60,7 @@ The [pull request template](.github/pull_request_template.md) is the checklist f
- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`.
- Run the tests before opening the pull request and describe what was tested (OS, title or homebrew).
- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form.
- Changes to the AGC driver, the shader recompiler or VideoOut name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging.
- Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments.
- Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it.

View File

@ -2,7 +2,9 @@
Tool for automatic executables porting to Linux and Windows.
[**Online converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
![anyps5_collage1](https://github.com/user-attachments/assets/0f8e13e8-1cb7-4c69-8075-39978e138994)
[Discord](https://discord.gg/BHFztBPUe)

View File

@ -25,7 +25,7 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i
target_compile_features(nid_patcher PRIVATE cxx_std_20)
if(MINGW)
target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++)
target_link_options(nid_patcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables)
check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS)
@ -179,23 +179,49 @@ if(BUILD_TESTING)
set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
if(MINGW)
target_compile_options(nid_buffer_validation_tests PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests)
set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $<TARGET_FILE:nid_patcher>)
add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $<TARGET_FILE:nid_patcher>)
add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $<TARGET_FILE:nid_patcher>)
set_tests_properties(nid_patcher_native_paths PROPERTIES TIMEOUT 20)
endif()
add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp)
add_dependencies(patched_libraries_tests libs)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $<TARGET_FILE:nid_patcher>)
endif()
if(MINGW)
target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables)
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
if(Python3_Interpreter_FOUND)
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
endif()
add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp)
target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_tests)
add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>)
endif()
if(WIN32)
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
@ -219,6 +245,10 @@ if(BUILD_TESTING)
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
configure_windows_unwind(guest_offline_imports_tests)
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $<TARGET_FILE:nid_patcher>)
endif()
if(WIN32)
set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
@ -411,6 +441,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePosixForWebKit>")
endif()
add_test_executable(MemoryPoolTest
tests/MemoryPoolTest.cpp
prx/libkernel/DirectMemory/MemoryPool.cpp
)
target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(MINGW)
target_compile_options(MemoryPoolTest PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest)
add_test_executable(guest_acm_tests tests/GuestAcm.cpp)
target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc)

View File

@ -0,0 +1,15 @@
#ifndef NID_NATIVEPATH_HPP
#define NID_NATIVEPATH_HPP
#include <filesystem>
#include <string>
namespace Nid::Internal {
inline std::filesystem::path NativePath(const std::string& path) {
return std::filesystem::path(std::u8string(path.begin(), path.end()));
}
}
#endif

View File

@ -61,7 +61,7 @@ inline std::string StripNidPostfix(const std::string& name) {
template<typename T>
T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("read out of bounds");
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("read out of bounds");
T v;
std::memcpy(&v, buf.data() + offset, sizeof(T));
return v;
@ -69,7 +69,7 @@ T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
template<typename T>
void Write(std::vector<std::uint8_t>& buf, std::size_t offset, const T& v) {
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("write out of bounds");
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("write out of bounds");
std::memcpy(buf.data() + offset, &v, sizeof(T));
}
@ -78,6 +78,7 @@ inline std::string ReadCStr(const std::vector<std::uint8_t>& buf, std::size_t of
std::string s;
while (offset < buf.size() && buf[offset] != 0u)
s.push_back(static_cast<char>(buf[offset++]));
if (offset == buf.size()) throw std::runtime_error("unterminated cstr");
return s;
}

View File

@ -15,6 +15,11 @@ namespace Nid {
namespace {
void RequireElfRange(const std::vector<std::uint8_t>& elf, std::uint64_t offset, std::uint64_t size, const char* table) {
if (offset > elf.size() || size > elf.size() - offset)
throw std::runtime_error(std::string("invalid ELF ") + table + " range");
}
std::uint32_t GnuHash(const std::string& name) {
std::uint32_t h = 5381u;
for (const unsigned char c : name)
@ -36,6 +41,18 @@ GnuHashLayout ReadGnuHashLayout(const std::vector<std::uint8_t>& elf, std::size_
return layout;
}
void ValidateGnuHash(const std::vector<std::uint8_t>& elf, std::size_t offset, std::size_t size, std::size_t symCount) {
if (offset > elf.size() || size > elf.size() - offset || size < 16u)
throw std::runtime_error("invalid gnu.hash section range");
const auto layout = ReadGnuHashLayout(elf, offset);
if (layout.SymOffset > symCount || !layout.BloomSize || (layout.BloomSize & (layout.BloomSize - 1u)) ||
layout.BloomShift >= 32u || (!layout.NBuckets && layout.SymOffset != symCount))
throw std::runtime_error("invalid gnu.hash geometry");
const std::uint64_t required = 16u + std::uint64_t{layout.BloomSize} * 8u +
std::uint64_t{layout.NBuckets} * 4u + (symCount - layout.SymOffset) * 4u;
if (required > size) throw std::runtime_error("gnu.hash section too small to rebuild in place");
}
void ReorderDynSymForGnuHash(
std::vector<std::uint8_t>& elf,
std::size_t dynSymOffset,
@ -204,49 +221,65 @@ void RemapDynamicNeededOffsets(
}
}
struct VersionNameField {
std::size_t Offset;
std::uint32_t Name;
};
std::vector<VersionNameField> ReadVersionNameFields(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
using namespace Internal;
std::vector<VersionNameField> fields;
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const auto section = Read<Elf64_Shdr>(elf, static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr));
if (section.sh_type != kShtGnuVerneed || section.sh_size == 0u) continue;
if (section.sh_offset > elf.size() || section.sh_size > elf.size() - section.sh_offset)
throw std::runtime_error("invalid .gnu.version_r section range");
const auto location = [&](std::uint64_t relative, std::size_t size) {
if (relative > section.sh_size || size > section.sh_size - relative)
throw std::runtime_error(".gnu.version_r record outside section");
return static_cast<std::size_t>(section.sh_offset + relative);
};
const auto advance = [&](std::uint64_t current, std::uint32_t step, std::size_t size) {
if (step < size || step > section.sh_size - current)
throw std::runtime_error("invalid .gnu.version_r record link");
return current + step;
};
std::uint64_t current = 0u;
while (true) {
const auto offset = location(current, sizeof(Elf64_Verneed));
const auto need = Read<Elf64_Verneed>(elf, offset);
if (need.vn_version != 1u) throw std::runtime_error("unsupported .gnu.version_r structure version");
fields.push_back({offset + offsetof(Elf64_Verneed, vn_file), need.vn_file});
if (need.vn_cnt != 0u) {
auto auxiliary = advance(current, need.vn_aux, sizeof(Elf64_Verneed));
for (std::uint16_t index = 0u; index < need.vn_cnt; ++index) {
const auto auxOffset = location(auxiliary, sizeof(Elf64_Vernaux));
const auto aux = Read<Elf64_Vernaux>(elf, auxOffset);
fields.push_back({auxOffset + offsetof(Elf64_Vernaux, vna_name), aux.vna_name});
if (index + 1u == need.vn_cnt) {
if (aux.vna_next != 0u) throw std::runtime_error(".gnu.version_r auxiliary count mismatch");
} else {
auxiliary = advance(auxiliary, aux.vna_next, sizeof(Elf64_Vernaux));
}
}
}
if (need.vn_next == 0u) break;
current = advance(current, need.vn_next, sizeof(Elf64_Verneed));
}
}
return fields;
}
void RemapVerneedOffsets(
std::vector<std::uint8_t>& elf,
const Elf64_Ehdr& ehdr,
const std::vector<std::pair<std::uint32_t, std::uint32_t>>& oldToNewOffset
) {
using namespace Internal;
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != kShtGnuVerneed) continue;
const std::size_t verneedOffset = static_cast<std::size_t>(shdr.sh_offset);
std::size_t entryOffset = verneedOffset;
while (true) {
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
const auto mappedFile = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == verneed.vn_file; }
);
if (mappedFile != oldToNewOffset.end())
Write(elf, entryOffset + offsetof(Elf64_Verneed, vn_file), mappedFile->second);
std::size_t auxOffset = entryOffset + verneed.vn_aux;
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
const auto mappedName = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == vernaux.vna_name; }
);
if (mappedName != oldToNewOffset.end())
Write(elf, auxOffset + offsetof(Elf64_Vernaux, vna_name), mappedName->second);
if (vernaux.vna_next == 0u) break;
auxOffset += vernaux.vna_next;
}
if (verneed.vn_next == 0u) break;
entryOffset += verneed.vn_next;
}
for (const auto& field : ReadVersionNameFields(elf, ehdr)) {
const auto mapped = std::find_if(oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == field.Name; });
if (mapped != oldToNewOffset.end()) Write(elf, field.Offset, mapped->second);
}
}
@ -275,35 +308,8 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
}
std::vector<std::uint32_t> CollectVerneedNameOffsets(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
using namespace Internal;
std::vector<std::uint32_t> offsets;
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != kShtGnuVerneed) continue;
std::size_t entryOffset = static_cast<std::size_t>(shdr.sh_offset);
while (true) {
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
offsets.push_back(verneed.vn_file);
std::size_t auxOffset = entryOffset + verneed.vn_aux;
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
offsets.push_back(vernaux.vna_name);
if (vernaux.vna_next == 0u) break;
auxOffset += vernaux.vna_next;
}
if (verneed.vn_next == 0u) break;
entryOffset += verneed.vn_next;
}
}
for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name);
return offsets;
}
@ -316,6 +322,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
if (elf[4] != 2) throw std::runtime_error("not ELF64");
const auto ehdr = Read<Elf64_Ehdr>(elf, 0u);
if (ehdr.e_ident[5] != 1 || ehdr.e_shentsize != sizeof(Elf64_Shdr))
throw std::runtime_error("unsupported ELF byte order or section header size");
RequireElfRange(elf, ehdr.e_shoff, std::uint64_t{ehdr.e_shnum} * sizeof(Elf64_Shdr), "section table");
std::size_t dynSymOffset = 0u, dynSymSize = 0u;
std::size_t gnuHashOffset = 0u, gnuHashSize = 0u;
@ -326,6 +335,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type == kShtDynsym) {
if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum)
throw std::runtime_error("invalid ELF dynsym layout");
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynsym");
dynSymOffset = static_cast<std::size_t>(shdr.sh_offset);
dynSymSize = static_cast<std::size_t>(shdr.sh_size);
dynSymSectionIndex = i;
@ -333,7 +345,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
if (shdr.sh_type == kShtGnuHash) {
gnuHashOffset = static_cast<std::size_t>(shdr.sh_offset);
gnuHashSize = static_cast<std::size_t>(shdr.sh_size);
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
}
if (shdr.sh_type == kShtGnuVersym)
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
@ -345,6 +356,10 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym);
if (symCount == 0u) throw std::runtime_error(".dynsym is empty");
if (gnuHashOffset != 0u) {
ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount);
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
}
const std::size_t dynStrSectionLink = [&]() -> std::size_t {
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
@ -359,6 +374,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
{
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != 3) throw std::runtime_error("invalid ELF dynstr section type");
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynstr");
dynStrOffset = static_cast<std::size_t>(shdr.sh_offset);
dynStrSize = static_cast<std::size_t>(shdr.sh_size);
}
@ -427,7 +444,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
const auto addName = [&](std::uint32_t oldOffset, std::string value) {
const auto existing = std::find_if(
names.begin(), names.end(),
[&](const auto& entry) { return entry.first == oldOffset; }
[&](const auto& entry) { return entry.first == oldOffset && entry.second == value; }
);
if (existing == names.end()) names.emplace_back(oldOffset, std::move(value));
};
@ -463,8 +480,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
newDynStr.push_back(0u);
}
std::vector<std::pair<std::uint32_t, std::uint32_t>> oldToNewOffset;
for (const auto& name : names) oldToNewOffset.emplace_back(name.first, valueOffsets.at(name.second));
std::vector<std::pair<std::uint32_t, std::uint32_t>> preservedToNewOffset;
for (const auto offset : preservedOffsets)
preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset)));
if (newDynStr.size() > dynStrSize) {
throw std::runtime_error(
@ -475,25 +493,15 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
newDynStr.resize(dynStrSize, 0u);
std::vector<std::string> newSymbolNames(symCount);
for (std::size_t i = 1u; i < symCount; ++i) {
const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym);
const auto sym = Read<Elf64_Sym>(elf, symOffset);
if (sym.st_name == 0u) continue;
const auto mapped = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == sym.st_name; }
);
if (mapped == oldToNewOffset.end()) continue;
newSymbolNames[i] = ReadCStr(newDynStr, mapped->second);
Write(elf, symOffset + offsetof(Elf64_Sym, st_name), mapped->second);
for (const auto& use : uses) {
newSymbolNames[use.symIndex] = use.newValue;
Write(elf, dynSymOffset + use.symIndex * sizeof(Elf64_Sym) + offsetof(Elf64_Sym, st_name), valueOffsets.at(use.newValue));
}
std::memcpy(elf.data() + dynStrOffset, newDynStr.data(), newDynStr.size());
RemapDynamicNeededOffsets(elf, ehdr, oldToNewOffset);
RemapVerneedOffsets(elf, ehdr, oldToNewOffset);
RemapDynamicNeededOffsets(elf, ehdr, preservedToNewOffset);
RemapVerneedOffsets(elf, ehdr, preservedToNewOffset);
if (gnuHashOffset != 0u) {
const auto bucketCount = Read<std::uint32_t>(elf, gnuHashOffset);

View File

@ -3,6 +3,7 @@
#include <nid/PeNidPatcher.hpp>
#include <nid/MachONidPatcher.hpp>
#include <nid/NidPatcherUtils.hpp>
#include <nid/NativePath.hpp>
#include <bit>
#include <cstring>
#include <fstream>
@ -49,7 +50,12 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
if (!directory.VirtualAddress || directory.Size < sizeof(PeExportDirectory)) throw std::runtime_error("missing reference PE exports");
const auto sections = optional + optionalSize;
RequireRange(binary, sections, std::uint64_t{sectionCount} * sizeof(PeSectionHeader));
const auto headersSize = ReadValue<std::uint32_t>(binary, optional + 60);
const auto mapRva = [&](std::uint32_t rva, std::uint64_t size) -> std::uint64_t {
if (rva < headersSize && size <= headersSize - rva) {
RequireRange(binary, rva, size);
return rva;
}
for (std::uint16_t index = 0; index < sectionCount; ++index) {
const auto section = ReadValue<PeSectionHeader>(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader));
if (rva < section.VirtualAddress) continue;
@ -64,14 +70,20 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
const auto table = ReadValue<PeExportDirectory>(binary, mapRva(directory.VirtualAddress, sizeof(PeExportDirectory)));
if (!table.NumberOfNames) throw std::runtime_error("reference PE has no named exports");
const auto names = mapRva(table.AddressOfNames, std::uint64_t{table.NumberOfNames} * 4);
const auto ordinals = mapRva(table.AddressOfNameOrdinals, std::uint64_t{table.NumberOfNames} * 2);
const auto functions = mapRva(table.AddressOfFunctions, std::uint64_t{table.NumberOfFunctions} * 4);
std::unordered_set<std::string> result;
for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) {
const auto ordinal = ReadValue<std::uint16_t>(binary, ordinals + std::uint64_t{index} * 2);
if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal");
if (!ReadValue<std::uint32_t>(binary, functions + std::uint64_t{ordinal} * 4)) continue;
const auto rva = ReadValue<std::uint32_t>(binary, names + std::uint64_t{index} * 4);
const auto offset = mapRva(rva, 1);
auto name = ReadName(binary, offset, binary.size() - offset);
mapRva(rva, name.size() + 1);
if (!result.insert(name).second) throw std::runtime_error("duplicate reference export: " + name);
}
if (result.empty()) throw std::runtime_error("reference PE has no named exports");
return result;
}
@ -110,7 +122,7 @@ std::string NormalizeExportName(const std::string& name) {
std::unordered_set<std::string> ReadExportExclusions(const std::string& path) {
if constexpr (std::endian::native != std::endian::little) throw std::runtime_error("export references require a little-endian host");
std::ifstream input(path, std::ios::binary | std::ios::ate);
std::ifstream input(Internal::NativePath(path), std::ios::binary | std::ios::ate);
if (!input) throw std::runtime_error("cannot open export reference: " + path);
const auto size = input.tellg();
if (size <= 0) throw std::runtime_error("empty or unreadable export reference: " + path);

View File

@ -1,5 +1,6 @@
#include <nid/BinaryPatcherFactory.hpp>
#include <nid/ExportExclusions.hpp>
#include <nid/NativePath.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>
@ -9,20 +10,20 @@
namespace {
std::vector<std::uint8_t> _readFile(const std::string& path) {
std::ifstream f(path, std::ios::binary);
std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary);
if (!f) throw std::runtime_error("cannot open: " + path);
return {std::istreambuf_iterator<char>(f), {}};
}
void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data) {
std::ofstream f(path, std::ios::binary);
std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary);
if (!f) throw std::runtime_error("cannot write: " + path);
f.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
}
}
int main(int argc, char* argv[]) {
int Run(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library>] <file> [file2 ...]\n";
return 1;
@ -65,3 +66,27 @@ int main(int argc, char* argv[]) {
return 0;
}
#ifdef _WIN32
int wmain(int argc, wchar_t* argv[]) {
try {
std::vector<std::string> values;
values.reserve(static_cast<std::size_t>(argc));
for (int index = 0; index < argc; ++index) {
const auto utf8 = std::filesystem::path(argv[index]).u8string();
values.emplace_back(utf8.begin(), utf8.end());
}
std::vector<char*> arguments;
arguments.reserve(values.size());
for (auto& value : values) arguments.push_back(value.data());
return Run(argc, arguments.data());
} catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 2;
}
}
#else
int main(int argc, char* argv[]) {
return Run(argc, argv);
}
#endif

View File

@ -0,0 +1,60 @@
#include <nid/NidPatcherUtils.hpp>
#include <cstdint>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <vector>
namespace {
void Require(bool condition) {
if (!condition) throw std::runtime_error("NID buffer validation failed");
}
template<typename TAction>
void RequireRejected(TAction action) {
try { action(); }
catch (const std::runtime_error&) { return; }
throw std::runtime_error("Malformed NID buffer was accepted");
}
}
int main() {
try {
using namespace Nid::Internal;
const std::vector<std::uint8_t> unterminated{'n', 'a', 'm', 'e'};
RequireRejected([&] { ReadCStr(unterminated, 0); });
RequireRejected([&] { ReadCStr(unterminated, 3); });
const std::vector<std::uint8_t> strings{'a', 0, 'b', 'c', 0};
Require(ReadCStr(strings, 0) == "a");
Require(ReadCStr(strings, 1).empty());
Require(ReadCStr(strings, 2) == "bc");
Require(ReadCStr(strings, 4).empty());
RequireRejected([&] { ReadCStr(strings, strings.size()); });
RequireRejected([&] { ReadCStr({}, 0); });
std::vector<std::uint8_t> bytes(8, 0xa5);
const std::uint64_t value = 0x1020304050607080;
Write(bytes, 0, value);
Require(Read<std::uint64_t>(bytes, 0) == value);
Write<std::uint32_t>(bytes, 4, 0x12345678);
Require(Read<std::uint32_t>(bytes, 4) == 0x12345678);
const auto before = bytes;
for (const auto offset : {std::size_t{5}, std::size_t{8}, std::size_t{9},
std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max() - 1,
std::numeric_limits<std::size_t>::max() - 3}) {
RequireRejected([&] { Read<std::uint32_t>(bytes, offset); });
RequireRejected([&] { Write<std::uint32_t>(bytes, offset, 0); });
Require(bytes == before);
}
RequireRejected([&] { Read<std::uint64_t>(bytes, 1); });
std::vector<std::uint8_t> empty;
RequireRejected([&] { Read<std::uint8_t>(empty, 0); });
RequireRejected([&] { Write<std::uint8_t>(empty, 0, 0); });
std::cout << "NID buffer validation passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -0,0 +1,48 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, SHOFF
class ElfLayoutTests(unittest.TestCase):
def test_declared_layout_must_match_supported_tables(self):
changes = ((5, "B", 2), (0x3a, "H", 32),
(SHOFF + 64 + 56, "Q", 8),
(SHOFF + 64 + 32, "Q", len(symbols(False)) * 24 + 1),
(SHOFF + 128 + 4, "I", 1))
for offset, kind, value in changes:
with self.subTest(offset=offset, value=value):
image = build_image(symbols(False), False)
struct.pack_into("<" + kind, image, offset, value)
self.reject(image)
def test_table_ranges_and_links_are_checked(self):
changes = ((0x28, "Q", 0x10000),
(SHOFF + 64 + 24, "Q", 0xff0),
(SHOFF + 64 + 40, "I", 100),
(SHOFF + 128 + 24, "Q", 0xff0),
(SHOFF + 128 + 32, "Q", 0xffffffffffffffff))
for offset, kind, value in changes:
with self.subTest(offset=offset, value=value):
image = build_image(symbols(False), False)
struct.pack_into("<" + kind, image, offset, value)
self.reject(image)
def reject(self, image):
result, after = patch(PATCHER, bytes(image), "layout.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertRegex(result.stderr.decode(), "ELF|dynsym|dynstr")
self.assertEqual(after, image)
def test_valid_supported_layout_still_patches(self):
image = build_image(symbols(False), False)
result, after = patch(PATCHER, bytes(image), "valid-layout.so")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotEqual(after, image)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -0,0 +1,75 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash
class GnuHashTests(unittest.TestCase):
def image(self, buckets=4, bloom=1, shift=6, symoffset=3):
image = build_image(symbols(False), False)
count = len(symbols(False))
struct.pack_into("<IIII", image, HASH_OFFSET, buckets, symoffset, bloom, shift)
size = 16 + 8 * bloom + 4 * buckets + 4 * max(0, count - symoffset)
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
return image
def test_non_power_of_two_bloom_is_rejected(self):
self.reject(self.image(bloom=3))
def test_hash_shift_outside_word_is_rejected(self):
self.reject(self.image(shift=32))
def test_unsafe_geometry_is_rejected(self):
for fields in ({"bloom": 0}, {"buckets": 0}, {"symoffset": 100}):
with self.subTest(fields=fields):
self.reject(self.image(**fields))
def test_declared_section_extent_is_checked(self):
for size in (12, 16, 0x10000):
with self.subTest(size=size):
image = self.image()
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
self.reject(image)
def reject(self, image):
result, after = patch(PATCHER, bytes(image), "geometry.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertIn(b"gnu.hash", result.stderr)
self.assertEqual(after, image)
def test_rebuilt_hash_finds_every_export(self):
image = self.image(bloom=2)
result, after = patch(PATCHER, image, "valid.so")
self.assertEqual(result.returncode, 0, result.stderr)
entries = read_dynsym(after)
buckets, first, bloom, shift = struct.unpack_from("<4I", after, HASH_OFFSET)
bucket_offset = HASH_OFFSET + 16 + bloom * 8
chain_offset = bucket_offset + buckets * 4
for index in range(first, len(entries)):
name = entries[index][0]
hashed = gnu_hash(name)
word = struct.unpack_from("<Q", after, HASH_OFFSET + 16 + ((hashed // 64) % bloom) * 8)[0]
self.assertTrue(word & (1 << (hashed % 64)))
self.assertTrue(word & (1 << ((hashed >> shift) % 64)))
start = struct.unpack_from("<I", after, bucket_offset + (hashed % buckets) * 4)[0]
found = False
for candidate in range(start, len(entries)):
value = struct.unpack_from("<I", after, chain_offset + (candidate - first) * 4)[0]
if (value | 1) == (hashed | 1) and entries[candidate][0] == name:
found = True
if value & 1:
break
self.assertTrue(found, name)
def test_empty_hash_with_no_buckets_is_supported(self):
image = self.image(buckets=0, symoffset=len(symbols(False)))
result, after = patch(PATCHER, image, "empty.so")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(struct.unpack_from("<I", after, HASH_OFFSET)[0], 0)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -0,0 +1,56 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from test_macho_nids import build_dylib, exports_of, nid
PATCHER = Path(sys.argv.pop(1)).resolve()
class NativePathTests(unittest.TestCase):
def invoke(self, *arguments):
return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)],
capture_output=True, encoding="utf-8", errors="replace", timeout=10)
def test_non_ascii_directory_and_filename_patch_the_intended_file(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary) / "\u6e2c\u8a66"
root.mkdir()
file = root / "\u043c\u043e\u0434\u0443\u043b\u044c.prx"
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], []))
result = self.invoke(file)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(file.read_bytes()), {"_" + nid("puts"): (0, 0x100)})
self.assertIn(str(file), result.stdout)
self.assertEqual(list(root.iterdir()), [file])
def test_non_ascii_export_reference_preserves_the_named_export(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
reference = root / "\u6e2c\u8a66-reference.prx"
original = build_dylib([("_puts", 0, 0x100)], [])
reference.write_bytes(original)
file = root / "target.prx"
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
result = self.invoke("--preserve-exports", reference, file)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(file.read_bytes()), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
self.assertEqual(reference.read_bytes(), original)
def test_missing_unicode_input_does_not_modify_another_file(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
existing = root / "existing.prx"
existing.write_bytes(b"unchanged")
missing = root / "\u6e2c\u8a66-missing.prx"
result = self.invoke(missing)
self.assertEqual(result.returncode, 2)
self.assertIn(str(missing), result.stderr)
self.assertFalse(missing.exists())
self.assertEqual(existing.read_bytes(), b"unchanged")
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,69 @@
import struct
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from test_macho_nids import build_dylib, exports_of, nid
def reference(*, headers=False, address=0x2000, ordinal=0):
image = bytearray(1280)
image[:2] = b"MZ"
struct.pack_into("<I", image, 0x3c, 0x40)
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664, 2, 0, 0, 0, 240, 0x2022)
optional = 0x58
struct.pack_into("<H", image, optional, 0x20b)
struct.pack_into("<I", image, optional + 60, 512)
struct.pack_into("<I", image, optional + 108, 16)
start, rva = (416, 416) if headers else (512, 0x1000)
struct.pack_into("<II", image, optional + 112, rva, 80)
for index, (name, va, offset, size) in enumerate(((b".edata", 0x1000, 512, 256), (b".text", 0x2000, 768, 512))):
struct.pack_into("<8sIIIIIIHHI", image, optional + 240 + index * 40,
name, size, va, size, offset, 0, 0, 0, 0, 0x40000040)
struct.pack_into("<IIHHIIIIIII", image, start, 0, 0, 0, 0, rva + 50, 1, 1, 1, rva + 40, rva + 44, rva + 48)
struct.pack_into("<IIH", image, start + 40, address, rva + 50, ordinal)
name = b"puts_nid_postfix\0"
image[start + 50:start + 50 + len(name)] = name
return image
class ReferenceTests(unittest.TestCase):
def patch_with_reference(self, data):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source, ref = root / "target.dylib", root / "reference.bin"
original = build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])
source.write_bytes(original)
ref.write_bytes(data)
result = subprocess.run([str(PATCHER), "libc", str(source), "--preserve-exports", str(ref)], capture_output=True, text=True)
self.assertEqual(ref.read_bytes(), data)
return result, source.read_bytes(), original
def test_header_resident_export_tables(self):
result, image, _ = self.patch_with_reference(reference(headers=True))
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(image), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
def test_section_resident_export_tables(self):
result, image, _ = self.patch_with_reference(reference())
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("_puts_nid_postfix", exports_of(image))
def test_null_function_slot_does_not_preserve_a_name(self):
result, image, original = self.patch_with_reference(reference(address=0))
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("no named exports", result.stdout + result.stderr)
self.assertEqual(image, original)
def test_invalid_name_ordinal_is_rejected_without_changing_target(self):
result, image, original = self.patch_with_reference(reference(ordinal=1))
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("ordinal", result.stdout + result.stderr)
self.assertEqual(image, original)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -0,0 +1,58 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSYM_OFFSET, read_dynsym, sections, DEFINITIONS
class SharedStringTests(unittest.TestCase):
def test_dynamic_names_keep_original_string_when_export_shares_offset(self):
for tag in (1, 14, 15, 29):
with self.subTest(tag=tag):
data = build_image(symbols(False), False)
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
struct.pack_into("<H", data, 0x3c, 5)
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 6, 2, 0, 0x580, 32, 2, 0, 8, 16)
struct.pack_into("<qQqQ", data, 0x580, tag, name_offset, 0, 0)
result, after = patch(PATCHER, bytes(data), "shared-dynamic.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
new_offset = struct.unpack_from("<Q", after, 0x588)[0]
start = strings[4] + new_offset
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
self.assertNotEqual(renamed, DEFINITIONS[0][0])
def test_version_requirement_strings_keep_original_names(self):
data = build_image(symbols(False), False)
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
struct.pack_into("<H", data, 0x3c, 5)
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, 0x580, 32, 2, 1, 8, 0)
struct.pack_into("<HHIII", data, 0x580, 1, 1, name_offset, 16, 0)
struct.pack_into("<IHHII", data, 0x590, 0, 0, 2, name_offset, 0)
result, after = patch(PATCHER, bytes(data), "shared-version.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for field in (0x584, 0x598):
offset = struct.unpack_from("<I", after, field)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
self.assertNotEqual(renamed, DEFINITIONS[0][0])
def test_local_symbol_keeps_name_shared_with_renamed_global(self):
entries = symbols(False)
entries[1] = (DEFINITIONS[0][0], 1, 9, 2, 0x1500)
data = build_image(entries, False)
data[DYNSYM_OFFSET + 24 + 4] = 2
result, after = patch(PATCHER, bytes(data), "shared-local.so")
self.assertEqual(result.returncode, 0, result.stderr)
names = {value: name for name, _, value in read_dynsym(after)}
self.assertEqual(names[0x1500], DEFINITIONS[0][0])
self.assertNotEqual(names[DEFINITIONS[0][2]], DEFINITIONS[0][0])
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -0,0 +1,76 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections
VERNEED = 0x580
def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0):
data = build_image(symbols(False), False)
strings = sections(data)[2]
filename = strings[5]
names = b"libfixture.so\0FIXTURE_1\0"
data[DYNSTR_OFFSET + filename:DYNSTR_OFFSET + filename + len(names)] = names
struct.pack_into("<Q", data, SHOFF + 2 * 64 + 32, filename + len(names))
struct.pack_into("<H", data, 0x3c, 5)
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, VERNEED, size, 2, 1, 8, 0)
struct.pack_into("<HHIII", data, VERNEED, version, count, filename, auxiliary, nextrecord)
struct.pack_into("<IHHII", data, VERNEED + auxiliary, 0, 0, 2, filename + len(b"libfixture.so\0"), auxnext)
return data
class VersionRequirementsTests(unittest.TestCase):
def test_auxiliary_record_cannot_leave_declared_section(self):
self.reject(image(auxiliary=32))
def test_auxiliary_count_must_match_chain(self):
self.reject(image(count=2))
def test_unsupported_structure_version_is_rejected(self):
self.reject(image(version=2))
def test_invalid_forward_links_and_section_ranges_are_rejected(self):
for fields in ({"auxnext": 16}, {"nextrecord": 64}, {"size": 15},
{"size": 0x10000}, {"auxiliary": 8}, {"count": 2, "auxnext": 8}):
with self.subTest(fields=fields):
self.reject(image(**fields))
def test_valid_multiple_auxiliaries_and_requirements(self):
data = image(count=2, size=80, auxnext=16, nextrecord=48)
original_name = struct.unpack_from("<I", data, VERNEED + 24)[0]
original_file = struct.unpack_from("<I", data, VERNEED + 4)[0]
struct.pack_into("<IHHII", data, VERNEED + 32, 0, 0, 3, original_name, 0)
struct.pack_into("<HHIII", data, VERNEED + 48, 1, 1, original_file, 16, 0)
struct.pack_into("<IHHII", data, VERNEED + 64, 0, 0, 4, original_name, 0)
result, after = patch(PATCHER, bytes(data), "multiple-versions.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for position, expected in ((4, b"libfixture.so"), (24, b"FIXTURE_1"),
(40, b"FIXTURE_1"), (52, b"libfixture.so"), (72, b"FIXTURE_1")):
offset = struct.unpack_from("<I", after, VERNEED + position)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)], expected)
def reject(self, data):
result, after = patch(PATCHER, bytes(data), "versions.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertIn(b"version_r", result.stderr)
self.assertEqual(after, data)
def test_valid_names_are_preserved_when_strings_are_rebuilt(self):
data = image()
result, after = patch(PATCHER, bytes(data), "valid-versions.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for position, expected in ((VERNEED + 4, b"libfixture.so"), (VERNEED + 16 + 8, b"FIXTURE_1")):
offset = struct.unpack_from("<I", after, position)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)], expected)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -33,6 +33,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/PacketInfo.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Platform.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp
@ -95,6 +96,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_semaphore_tests EXCLUDE_FROM_ALL tests/Semaphore.cpp)
target_include_directories(agc_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_semaphore_tests PRIVATE libSceAgc libc)
configure_windows_unwind(agc_semaphore_tests)
if(BUILD_TESTING)
set_target_properties(agc_semaphore_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_semaphore_tests COMMAND agc_semaphore_tests)
if(WIN32)
set_tests_properties(agc_semaphore_tests PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp)
target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc)

View File

@ -36,7 +36,6 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev
Agc::Command::CheckAddress(guestAddress, 8, __func__);
return Agc::Command::Emit(buf, 0x46u, {0x100u | eventType, static_cast<std::uint32_t>(guestAddress), static_cast<std::uint32_t>(guestAddress >> 32u)}, __func__);
}
Agc::Command::Require(address == nullptr, __func__, "this event does not use an address");
const auto eventIndex = eventType == 7 || eventType == 15 || eventType == 16 ? 0x400u : 0u;
return Agc::Command::Emit(buf, 0x46u, {eventIndex | eventType}, __func__);
}

View File

@ -0,0 +1,4 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
#endif

View File

@ -0,0 +1,61 @@
#include "prx/libSceAgc/Misc/include/Semaphore.hpp"
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <limits>
#include <mutex>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
namespace {
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
constexpr std::uint64_t semaphoreLabelBytes = 32u;
constexpr std::uint64_t semaphoreCounterBytes = 8u;
std::mutex semaphoreMutex;
std::uint8_t* semaphoreMemory = nullptr;
std::uint64_t semaphoreBytes = 0;
bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) {
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) {
if (memory[label + byte] != 0) return false;
}
}
return true;
}
}
extern "C" {
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
const auto address = reinterpret_cast<std::uintptr_t>(memory);
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown");
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
auto* labels = static_cast<std::uint8_t*>(memory);
Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown");
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
std::memset(labels + label, 0, semaphoreCounterBytes);
}
semaphoreMemory = labels;
semaphoreBytes = sizeInBytes;
return 0;
}
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown");
Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown");
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
return 0;
}
}

View File

@ -35,16 +35,6 @@ int APS5_VABI sceAgcGetDefaultCxStateFlat() {
return 0;
}
int APS5_VABI sceAgcGetSemaphoreLabel() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetAmmSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;

View File

@ -10,6 +10,8 @@
#include <cstring>
#include <string>
extern "C" std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address);
extern "C" std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
@ -641,6 +643,47 @@ void testDefaults() {
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
}
void testEventWrite() {
Storage storage;
const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1000));
check(samplePacket == storage.words.data(), "incorrect event 0x38 return pointer");
check(storage.words[0] == 0xc0024600u, "incorrect event 0x38 packet header");
check(storage.words[1] == (0x100u | 0x38u), "incorrect event 0x38 first payload word");
check(storage.words[2] == 0x1000u, "incorrect event 0x38 low address word");
check(storage.words[3] == 0u, "incorrect event 0x38 high address word");
check(sceAgcDcbEventWriteGetSize(0x38) == 16, "incorrect event 0x38 size");
check(sceAgcDcbEventWriteGetSize(0x39) == 16, "incorrect event 0x39 size");
Storage sample39;
sceAgcDcbEventWrite(&sample39.buffer, 0x39, reinterpret_cast<const void*>(0x2008));
check(sample39.words[0] == 0xc0024600u && sample39.words[1] == (0x100u | 0x39u) && sample39.words[2] == 0x2008u && sample39.words[3] == 0u, "incorrect event 0x39 packet");
expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1001)); });
expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x40, nullptr); });
Storage partialFlush;
sceAgcDcbEventWrite(&partialFlush.buffer, 7, nullptr);
check(partialFlush.words[0] == 0xc0004600u && partialFlush.words[1] == 0x407u, "incorrect event 7 partial flush packet");
check(sceAgcDcbEventWriteGetSize(7) == 8, "incorrect event 7 size");
Storage partialFlush15;
sceAgcDcbEventWrite(&partialFlush15.buffer, 15, nullptr);
check(partialFlush15.words[0] == 0xc0004600u && partialFlush15.words[1] == 0x40fu, "incorrect event 15 partial flush packet");
Storage partialFlush16;
sceAgcDcbEventWrite(&partialFlush16.buffer, 16, nullptr);
check(partialFlush16.words[0] == 0xc0004600u && partialFlush16.words[1] == 0x410u, "incorrect event 16 partial flush packet");
Storage cacheFlushNull;
sceAgcDcbEventWrite(&cacheFlushNull.buffer, 0x16, nullptr);
check(cacheFlushNull.words[0] == 0xc0004600u && cacheFlushNull.words[1] == 0x16u, "incorrect event 0x16 null address packet");
check(sceAgcDcbEventWriteGetSize(0x16) == 8, "incorrect event 0x16 size");
Storage cacheFlushWithAddress;
sceAgcDcbEventWrite(&cacheFlushWithAddress.buffer, 0x16, reinterpret_cast<const void*>(0x2d2a10000ull));
check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet");
}
}
int main(int argc, char** argv) {
@ -652,6 +695,7 @@ int main(int argc, char** argv) {
return 0;
}
testPackets();
testEventWrite();
testNop();
testClearState();
testIndexedIndirectDraws();

View File

@ -0,0 +1,164 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <memory>
#include <new>
#include <stdexcept>
#include <vector>
extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes);
extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label);
namespace {
constexpr std::uint64_t blockBytes = 0x4000u;
constexpr std::uint64_t labelBytes = 32u;
constexpr std::uint64_t counterBytes = 8u;
constexpr std::uint64_t memoryBytes = 2u * blockBytes;
constexpr std::uint64_t storageBytes = 4u * blockBytes;
constexpr std::uint32_t labelCount = static_cast<std::uint32_t>(memoryBytes / labelBytes);
constexpr std::uint8_t guardByte = 0xa5u;
constexpr std::uint8_t counterByte = 0x5au;
struct AlignedRelease {
void operator()(std::uint8_t* memory) const { ::operator delete(memory, std::align_val_t{blockBytes}); }
};
std::unique_ptr<std::uint8_t, AlignedRelease> storage;
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template <typename TAction>
void expectFailure(TAction action) {
try {
action();
} catch (const std::runtime_error& error) {
check(error.what()[0] != '\0', "empty exception message");
return;
}
throw std::runtime_error("expected an exception");
}
std::uint8_t* memory() {
return storage.get() + blockBytes;
}
std::vector<std::uint8_t> snapshot() {
return {storage.get(), storage.get() + storageBytes};
}
void checkUnchanged(const std::vector<std::uint8_t>& before, const char* message) {
check(std::equal(before.begin(), before.end(), storage.get()), message);
}
void fillStorage() {
std::fill(storage.get(), storage.get() + storageBytes, guardByte);
for (std::uint64_t label = 0; label < memoryBytes; label += labelBytes) {
std::fill(memory() + label, memory() + label + counterBytes, counterByte);
std::fill(memory() + label + counterBytes, memory() + label + labelBytes, std::uint8_t{0});
}
}
void testUnregistered() {
std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()};
expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); });
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output");
}
void testRegistrationRejections() {
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); });
expectFailure([] {
const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes);
});
checkUnchanged(before, "a rejected registration changed memory");
testUnregistered();
}
void testUnsettledClearing() {
for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) {
memory()[offset] = 0x77u;
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
checkUnchanged(before, "a registration with unsettled clearing changed memory");
testUnregistered();
memory()[offset] = 0;
}
}
void testRegistration() {
const auto before = snapshot();
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed");
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters");
check(std::equal(before.begin(), before.begin() + blockBytes, storage.get()), "registration wrote before the semaphore memory");
check(std::equal(before.begin() + blockBytes + memoryBytes, before.end(), memory() + memoryBytes), "registration wrote past the semaphore memory");
}
void testLabels() {
for (const std::uint32_t index : {0u, 1u, 7u, labelCount / 2u, labelCount - 1u}) {
std::array<void*, 3> labels{storage.get(), nullptr, storage.get()};
check(sceAgcGetSemaphoreLabel(index, &labels[1]) == 0, "a semaphore label lookup failed");
check(labels[1] == memory() + index * labelBytes, "incorrect semaphore label address");
check(labels[0] == storage.get() && labels[2] == storage.get(), "a semaphore label lookup wrote past its output");
}
}
void testLabelRejections() {
for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) {
void* label = storage.get();
expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); });
check(label == storage.get(), "a rejected semaphore label changed the output");
}
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
}
void testRepeatedRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
checkUnchanged(before, "a repeated registration changed memory");
void* label = nullptr;
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
}
}
int main() {
try {
storage.reset(static_cast<std::uint8_t*>(::operator new(storageBytes, std::align_val_t{blockBytes})));
fillStorage();
testUnregistered();
testRegistrationRejections();
testUnsettledClearing();
testRegistration();
testLabels();
testLabelRejections();
testRepeatedRegistration();
storage.reset();
LibcRunShutdown_nid_postfix();
std::puts("AGC semaphore tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
try { LibcRunShutdown_nid_postfix(); }
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
return 1;
}
}

View File

@ -325,6 +325,29 @@ foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
endforeach()
add_test_executable(agc_driver_register_key_tests EXCLUDE_FROM_ALL tests/RegisterKey.cpp)
target_include_directories(agc_driver_register_key_tests PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_driver_register_key_tests PRIVATE
libSceAgcDriver
libc
libkernel
)
set_target_properties(agc_driver_register_key_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_register_key_tests)
if(BUILD_TESTING)
set_target_properties(agc_driver_register_key_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_driver_register_key COMMAND agc_driver_register_key_tests)
set_tests_properties(agc_driver_register_key PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_VERIFY_REGISTER_KEYS=1")
if(WIN32)
set_tests_properties(agc_driver_register_key PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL
tests/Submit.cpp
)
@ -543,6 +566,11 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv
set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_masked_select_tests)
add_test_executable(agc_ir_reclaim_tests EXCLUDE_FROM_ALL tests/IrReclaim.cpp)
target_link_libraries(agc_ir_reclaim_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_ir_reclaim_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_ir_reclaim_tests)
add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp)
target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -705,6 +733,11 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_formatted_buffer_byte_stores_tests EXCLUDE_FROM_ALL tests/FormattedBufferByteStores.cpp)
target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_formatted_buffer_byte_stores_tests)
add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp)
target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc)
@ -834,6 +867,19 @@ target_link_libraries(agc_driver_cmask_fast_clear_tests PRIVATE
set_target_properties(agc_driver_cmask_fast_clear_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_cmask_fast_clear_tests)
set(agcBufferUpdateSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/BufferUpdate.comp)
set(agcBufferUpdateSpv ${agcTestShaderGeneratedRoot}/BufferUpdate.spv)
set(agcBufferUpdateHeader ${agcTestShaderGeneratedRoot}/BufferUpdate_spv.h)
add_custom_command(
OUTPUT "${agcBufferUpdateHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcBufferUpdateSpv}" "${agcBufferUpdateSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcBufferUpdateSpv} -DOUTPUT=${agcBufferUpdateHeader}
-DSYMBOL=BUFFER_UPDATE_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcBufferUpdateSource}" glslang-standalone
VERBATIM
)
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
tests/Recorder.cpp
tests/ResidentPresent.cpp
@ -841,6 +887,7 @@ add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
${agcSampleLodShaderHeader}
${agcSampleArrayShaderHeader}
${agcCoverShaderHeaders}
${agcBufferUpdateHeader}
)
target_include_directories(agc_driver_recorder_tests PRIVATE
@ -908,6 +955,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_storage_written_buffer_order PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_copied_direct_ordering COMMAND agc_driver_recorder_tests --copied-direct-only)
set_tests_properties(agc_copied_direct_ordering PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only)
set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only)
@ -916,6 +969,11 @@ if(BUILD_TESTING)
set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only)
set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
if(WIN32)
add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only)
set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only)
set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only)
@ -927,6 +985,9 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@ -987,47 +1048,11 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
tests/GuestSamplerResource.cpp
tests/TextureDetiler.cpp
tests/DepthSurfaceReuse.cpp
Graphics/src/Draw.cpp
Graphics/src/DepthSurface.cpp
Graphics/src/MultisampleTarget.cpp
Graphics/src/Pipeline.cpp
Graphics/src/PipelineLibrary.cpp
Graphics/src/State.cpp
Graphics/src/ColorTargetLayout.cpp
Graphics/src/ColorTargetTransfer.cpp
Graphics/src/Resources.cpp
Graphics/src/PlaceholderImages.cpp
Graphics/src/ShaderResources.cpp
Graphics/src/ShaderAddressResources.cpp
Graphics/src/GuestBufferMemory.cpp
Graphics/src/BdaResources.cpp
Graphics/src/BufferAddress.cpp
Graphics/src/ShaderValidation.cpp
Graphics/src/GuestTextureResource.cpp
Graphics/src/GuestSamplerResource.cpp
Graphics/src/TextureFormat.cpp
Graphics/src/DccMetadata.cpp
Graphics/src/TextureTiling.cpp
Graphics/src/TextureDetiler.cpp
Graphics/src/Texture.cpp
Graphics/src/PassHazards.cpp
Graphics/src/UnitShadow.cpp
Graphics/src/Sampler.cpp
Graphics/src/Recorder.cpp
Graphics/src/ParallelCompare.cpp
Graphics/src/ShaderInputState.cpp
${agcTextureDetileShaderHeader}
${agcCmaskClearShaderHeader}
${agcMeshArgumentsShaderHeader}
${agcTextureDetileImageShaderHeader}
${agcSampleCounterShaderHeader}
${agcSampleDumpsShaderHeader}
Execution/src/GuestMemory.cpp
Execution/src/Pm4.cpp
)
target_include_directories(agc_driver_graphics_tests PRIVATE
${LIBS_INCLUDE_DIR}
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
${agcTextureDetileGeneratedRoot}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${CMAKE_SOURCE_DIR}/core/shader/recompiler/IntermediateRepresentation/include
@ -1038,7 +1063,7 @@ target_include_directories(agc_driver_graphics_tests PRIVATE
)
target_link_libraries(agc_driver_graphics_tests PRIVATE
shader_recompiler_agc_driver
libSceAgcDriver
libc
libkernel
)
@ -1046,45 +1071,71 @@ target_link_libraries(agc_driver_graphics_tests PRIVATE
set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_graphics_tests)
foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag)
string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}")
string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol)
set(agcVertexRangeSpv ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}.spv)
set(agcVertexRangeHeader ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}_spv.h)
add_custom_command(
OUTPUT "${agcVertexRangeHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcVertexRangeSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcVertexRangeSpv} -DOUTPUT=${agcVertexRangeHeader}
-DSYMBOL=${agcVertexRangeSymbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}" glslang-standalone
VERBATIM
)
list(APPEND agcVertexRangeHeaders ${agcVertexRangeHeader})
endforeach()
target_sources(agc_driver_recorder_tests PRIVATE ${agcVertexRangeHeaders})
if(BUILD_TESTING)
add_test(NAME agc_driver_vertex_target_range_copied COMMAND agc_driver_recorder_tests --vertex-target-range-copied)
add_test(NAME agc_driver_vertex_target_range_imported COMMAND agc_driver_recorder_tests --vertex-target-range-imported)
set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL
tests/BdaDevice.cpp
tests/BdaShader.cpp
tests/BdaExecution.cpp
tests/BdaContracts.cpp
Execution/src/BdaFeatures.cpp
Graphics/src/Resources.cpp
Graphics/src/PlaceholderImages.cpp
Graphics/src/BufferAddress.cpp
)
target_include_directories(agc_driver_bda_device_tests PRIVATE
${LIBS_INCLUDE_DIR}
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_driver_bda_device_tests PRIVATE SDL2-static libc shader_recompiler_agc_driver)
target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc)
set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_bda_device_tests)
add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL
tests/Mesh.cpp
${agcDriverSources}
)
add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp)
target_include_directories(agc_driver_mesh_tests PRIVATE
${LIBS_INCLUDE_DIR}
${agcTextureDetileGeneratedRoot}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_driver_mesh_tests PRIVATE shader_recompiler_agc_driver SDL2-static libc libkernel jpeg_turbo ${CMAKE_DL_LIBS})
add_dependencies(agc_driver_mesh_tests agc_trace_revision)
target_link_libraries(agc_driver_mesh_tests PRIVATE libSceAgcDriver SDL2-static libc)
set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_mesh_tests)
set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build, by file name without extension; the others build a stub that reports a skip. Empty builds all")
file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp)
set(agcExecutionTests)
foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
get_filename_component(agcExecutionTestName ${agcExecutionTestSource} NAME_WE)
if(ANYPS5_AGC_EXECUTION_TESTS AND NOT agcExecutionTestName IN_LIST ANYPS5_AGC_EXECUTION_TESTS)
set(agcExecutionTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExecutionTestNotBuilt.cpp)
endif()
string(REGEX REPLACE "([a-z0-9])([A-Z])" "\\1_\\2" agcExecutionTestName "${agcExecutionTestName}")
string(TOLOWER "agc_driver_${agcExecutionTestName}_tests" agcExecutionTest)
add_test_executable(${agcExecutionTest} EXCLUDE_FROM_ALL ${agcExecutionTestSource})
@ -1099,6 +1150,11 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
configure_windows_unwind(${agcExecutionTest})
list(APPEND agcExecutionTests ${agcExecutionTest})
endforeach()
foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp)
message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution")
endif()
endforeach()
target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber)
@ -1169,7 +1225,7 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests})
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests})
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests")
continue()
endif()
@ -1182,6 +1238,11 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores)
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes)
set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30)
set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30)
@ -1227,11 +1288,25 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_layered_volume_vertex COMMAND agc_driver_layered_volume_draw_tests --vertex-only)
add_test(NAME agc_driver_layered_volume_vertex_no_gpl COMMAND agc_driver_layered_volume_draw_tests --vertex-only)
set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_property(TEST agc_driver_layered_volume_vertex_no_gpl APPEND PROPERTY ENVIRONMENT "APS5_NO_GPL=1")
if(WIN32)
set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode)
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv)
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests)
set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests)
@ -1288,8 +1363,10 @@ if(BUILD_TESTING)
add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure)
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure)
add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown)
set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(NOT WIN32)
@ -1503,8 +1580,11 @@ if(BUILD_TESTING)
configure_windows_unwind(agc_compute_pipeline_capture_tests)
add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_covered_centroid_state COMMAND agc_driver_graphics_tests --covered-centroid-state)
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
@ -1809,6 +1889,21 @@ add_test_executable(agc_position_invariance_tests EXCLUDE_FROM_ALL tests/Positio
target_link_libraries(agc_position_invariance_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_position_invariance_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_position_invariance_tests)
add_test_executable(agc_vertex_layer_export_tests EXCLUDE_FROM_ALL tests/VertexLayerExport.cpp)
target_link_libraries(agc_vertex_layer_export_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vertex_layer_export_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_vertex_layer_export_tests)
if(BUILD_TESTING)
set_target_properties(agc_vertex_layer_export_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_vertex_layer_export COMMAND agc_vertex_layer_export_tests)
set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING)
set_target_properties(agc_position_invariance_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_position_invariance COMMAND agc_position_invariance_tests)

View File

@ -8,6 +8,8 @@ namespace AgcDriver::DriverDetail {
double TraceMs();
std::uint64_t SkippedShader();
void require(bool condition, const char* reason);
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset);

View File

@ -75,6 +75,19 @@ struct DrawEntryCounters {
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
};
class DrawKeySightings {
public:
static constexpr std::size_t Ways = 4;
explicit DrawKeySightings(std::size_t keys);
bool SeenBefore(std::uint64_t key);
private:
std::vector<std::uint64_t> keys;
std::vector<std::uint8_t> filled;
std::vector<std::uint8_t> next;
std::size_t bucketMask = 0;
};
enum class DrawVerdict { Drawn, Nothing, Rejected };
struct StageCapture {

View File

@ -0,0 +1,12 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
namespace AgcDriver::DriverDetail {
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true);
}
#endif

View File

@ -115,6 +115,7 @@ private:
static std::size_t drawCacheEntries();
void accountDrawVariant(const DispatchVariant& variant, bool added);
void insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode);
bool admitDrawKey(std::uint64_t key);
void eraseDrawEntry(std::uint64_t key, const std::shared_ptr<DrawEntry>& expected);
std::shared_ptr<const DrawRecipe> findDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages);
void attachDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages, std::shared_ptr<const DrawRecipe> recipe);
@ -310,6 +311,7 @@ private:
std::list<std::uint64_t> drawOrder;
std::mutex drawCacheMutex;
std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0;
std::optional<DrawKeySightings> drawKeySightings;
DrawEntryCounters drawEntryCounters;

View File

@ -2,6 +2,8 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP
#include "Recompiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
@ -18,6 +20,10 @@ struct DwordPatternFill {
};
std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern);
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill);
}

View File

@ -67,6 +67,7 @@ struct PreparedShaders : PreparedShaderState {
std::mutex mutex;
std::condition_variable settled;
std::exception_ptr failure;
std::atomic<bool> demanded{false};
bool pending = false;
};
@ -107,7 +108,8 @@ std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const Shade
std::shared_ptr<const ShaderSnapshot> RegisteredHeader(const ShaderRegistry& registry, const Shader* shader);
std::shared_ptr<const ShaderSnapshot> RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list<std::uint8_t> types);
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
void SubmitRegistrationPreparation(std::function<void()> task);
void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared = nullptr);
void PrioritizeRegistrationPreparation(PreparedShaders& prepared);
void SubmitLibraryPrebuild(std::function<void()> task);
void CloseRegistrationPreparation();

View File

@ -72,6 +72,8 @@ struct UnchangedQuery {
};
bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
bool UnchangedSinceAll(std::span<const UnchangedQuery> queries);
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges);
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges, std::uint64_t generation);
// Returns the generation the blocks were stamped with (0 when the arena is not write-watched):
// UnchangedSince(range, it) holds until the next store over the range.
std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes);

View File

@ -9,6 +9,7 @@
#include <stdexcept>
#include <utility>
#include <map>
#include <memory>
#include <array>
#include <optional>
#include <span>
@ -23,6 +24,29 @@ public:
using mapped_type = std::uint32_t;
using value_type = std::pair<const std::uint32_t, std::uint32_t>;
class Mask {
public:
explicit Mask(std::vector<std::uint64_t> bits) : bits(std::move(bits)) {}
bool Contains(std::uint32_t offset) const { return offset / 64 < bits.size() && ((bits[offset / 64] >> (offset % 64)) & 1u) != 0; }
private:
std::vector<std::uint64_t> bits;
};
class Reference {
public:
Reference(Registers& owner, std::uint32_t offset) : owner(owner), offset(offset) {}
operator std::uint32_t() const { return owner.values[offset]; }
Reference& operator=(std::uint32_t value) { owner.insert_or_assign(offset, value); return *this; }
Reference& operator=(const Reference& other) { return *this = static_cast<std::uint32_t>(other); }
Reference& operator|=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) | value; }
Reference& operator&=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) & value; }
Reference& operator^=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) ^ value; }
Reference& operator+=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) + value; }
private:
Registers& owner;
std::uint32_t offset;
};
class const_iterator {
public:
using iterator_category = std::forward_iterator_tag;
@ -57,6 +81,18 @@ public:
using iterator = const_iterator;
Registers() = default;
Registers(const Registers&) = default;
Registers& operator=(const Registers&) = default;
Registers(Registers&& other) noexcept : values(std::move(other.values)), present(std::move(other.present)), entries(std::exchange(other.entries, 0)), mask(std::move(other.mask)), fingerprint(std::exchange(other.fingerprint, 0)) {}
Registers& operator=(Registers&& other) noexcept {
if (this == &other) return *this;
values = std::move(other.values);
present = std::move(other.present);
entries = std::exchange(other.entries, 0);
mask = std::move(other.mask);
fingerprint = std::exchange(other.fingerprint, 0);
return *this;
}
Registers(std::initializer_list<std::pair<std::uint32_t, std::uint32_t>> entries) {
for (const auto& [offset, value] : entries) emplace(offset, value);
}
@ -83,27 +119,33 @@ public:
values.clear();
present.clear();
entries = 0;
fingerprint = 0;
}
std::pair<const_iterator, bool> emplace(std::uint32_t offset, std::uint32_t value) {
if (contains(offset)) return {const_iterator{this, offset}, false};
mark(offset) = value;
changeFingerprint(offset, value);
return {const_iterator{this, offset}, true};
}
std::pair<const_iterator, bool> insert_or_assign(std::uint32_t offset, std::uint32_t value) {
if (contains(offset)) {
if (values[offset] == value) return {const_iterator{this, offset}, false};
changeFingerprint(offset, values[offset]);
values[offset] = value;
changeFingerprint(offset, value);
return {const_iterator{this, offset}, false};
}
mark(offset) = value;
changeFingerprint(offset, value);
return {const_iterator{this, offset}, true};
}
std::uint32_t& operator[](std::uint32_t offset) {
if (contains(offset)) return values[offset];
return mark(offset) = 0;
Reference operator[](std::uint32_t offset) {
if (!contains(offset)) emplace(offset, 0);
return {*this, offset};
}
std::uint32_t& at(std::uint32_t offset) {
Reference at(std::uint32_t offset) {
if (!contains(offset)) throw std::out_of_range("register is not set");
return values[offset];
return {*this, offset};
}
const std::uint32_t& at(std::uint32_t offset) const {
if (!contains(offset)) throw std::out_of_range("register is not set");
@ -111,6 +153,7 @@ public:
}
std::size_t erase(std::uint32_t offset) {
if (!contains(offset)) return 0;
changeFingerprint(offset, values[offset]);
present[offset / 64] &= ~(std::uint64_t{1} << (offset % 64));
--entries;
return 1;
@ -123,8 +166,31 @@ public:
}
return a == end() && b == other.end();
}
std::uint64_t Fingerprint(const std::shared_ptr<const Mask>& selected) const {
if (mask != selected) {
fingerprint = RecomputeFingerprint(*selected);
mask = selected;
}
return fingerprint;
}
std::uint64_t RecomputeFingerprint(const Mask& selected) const {
std::uint64_t result = 0;
for (const auto [offset, value] : *this) {
if (selected.Contains(offset)) result ^= contribution(offset, value);
}
return result;
}
private:
static std::uint64_t contribution(std::uint32_t offset, std::uint32_t value) {
auto hash = ((static_cast<std::uint64_t>(offset) << 32u) | value) + 0x9e3779b97f4a7c15ull;
hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull;
hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull;
return hash ^ (hash >> 31u);
}
void changeFingerprint(std::uint32_t offset, std::uint32_t value) {
if (mask != nullptr && mask->Contains(offset)) fingerprint ^= contribution(offset, value);
}
static constexpr std::size_t End = ~std::size_t{0};
std::uint32_t& mark(std::uint32_t offset) {
if (offset >= values.size()) {
@ -147,6 +213,8 @@ private:
std::vector<std::uint32_t> values;
std::vector<std::uint64_t> present;
std::size_t entries = 0;
mutable std::shared_ptr<const Mask> mask;
mutable std::uint64_t fingerprint = 0;
};
inline Registers BuildInitialContextRegisters() {
@ -172,6 +240,7 @@ inline Registers BuildInitialContextRegisters() {
result.emplace(0x31c + 0xfu * i, 0);
}
for (std::uint32_t i = 0; i < 16; ++i) {
result.emplace(0x2fe + i, 0);
result.emplace(0x94 + 2 * i, 0x80000000);
result.emplace(0x95 + 2 * i, 0x40004000);
result.emplace(0xb4 + 2 * i, 0);

View File

@ -84,6 +84,7 @@ struct DrawRecipe {
std::optional<Graphics::State> masked;
std::set<std::uint32_t> fragmentOutputs;
VkPipelineStageFlags shaderStages = 0;
bool hasDepth = false;
};
}

View File

@ -222,7 +222,7 @@ public:
// blit-submit times after the flip packet; noted in-place reads checked
// (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence.
struct PresentStatistics {
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten;
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops;
double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs;
};
static PresentStatistics PresentCounts();

View File

@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include <cstdlib>
#include <stdexcept>
namespace AgcDriver::DriverDetail {
@ -9,6 +10,11 @@ double TraceMs() {
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - origin).count();
}
std::uint64_t SkippedShader() {
static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }();
return skipped;
}
void require(bool condition, const char* reason) {
if (!condition) {
throw std::runtime_error(std::string("AGC driver: ") + reason);

View File

@ -16,6 +16,7 @@ namespace AgcDriver::DriverDetail {
void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::uint64_t indirectArguments) {
PerformanceTimer timing("Driver.Dispatch");
const auto address = (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20d) & 0xffu) << 40u);
if (address == SkippedShader() && address != 0) return;
auto registeredShader = RegisteredProgram(*submission.shaders, address, {0});
if (!registeredShader) registeredShader = ReadRawShader(address);
const auto& snapshot = *registeredShader;

View File

@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
#include <cstdlib>
namespace AgcDriver::DriverDetail {
@ -94,6 +95,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
const auto& graphics = decode->state;
auto pixel = decode->pixel;
std::vector<DrawProgram> programs = decode->programs;
if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn();
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
if (!graphics.stages.mesh) return;
auto& words = programs.front().userData;
@ -145,7 +147,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return;
decodeReads[i].clear();
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr);
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207));
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207)) | graphics.layerExports;
};
if (!registerKey) {
for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i);

View File

@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp"
#include <algorithm>
#include <bit>
#include <cstdlib>
namespace AgcDriver::DriverDetail {
@ -56,6 +57,36 @@ void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) {
}
}
DrawKeySightings::DrawKeySightings(std::size_t keyCount) {
Graphics::Require(keyCount >= Ways, "draw key sightings need at least one bucket");
const auto buckets = std::bit_ceil(keyCount / Ways);
keys.assign(buckets * Ways, 0);
filled.assign(buckets, 0);
next.assign(buckets, 0);
bucketMask = buckets - 1;
}
bool DrawKeySightings::SeenBefore(std::uint64_t key) {
auto mixed = key;
mixed = (mixed ^ (mixed >> 30u)) * 0xbf58476d1ce4e5b9ull;
mixed = (mixed ^ (mixed >> 27u)) * 0x94d049bb133111ebull;
mixed ^= mixed >> 31u;
const auto bucket = static_cast<std::size_t>(mixed) & bucketMask;
auto* ways = keys.data() + bucket * Ways;
if (std::find(ways, ways + filled[bucket], key) != ways + filled[bucket]) return true;
ways[next[bucket]] = key;
next[bucket] = static_cast<std::uint8_t>((next[bucket] + 1u) % Ways);
if (filled[bucket] < Ways) ++filled[bucket];
return false;
}
bool Driver::admitDrawKey(std::uint64_t key) {
std::lock_guard cacheLock(drawCacheMutex);
if (drawCache.contains(key)) return true;
if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways);
return drawKeySightings->SeenBefore(key);
}
void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode) {
std::lock_guard cacheLock(drawCacheMutex);
auto& counters = drawEntryCounters;

View File

@ -20,7 +20,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st
program.binary,
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
localDevice->Target(),
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
@ -83,7 +83,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st
}
void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming) {
if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu)) {
if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) {
phaseTiming.Phase(DrawRowVectors);
std::uint64_t unstable = 0, mismatches = 0;
for (std::size_t i = 0; i < programs.size(); ++i) {

View File

@ -116,6 +116,7 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
product->pixel.dualSourceBlend = product->state.dualSourceBlend;
product->pixel.alphaToCoverage = product->state.alphaToCoverage;
if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state);
if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0;
return product;
}

View File

@ -26,7 +26,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(graphics.stages.tessellation.has_value());
if (graphics.stages.tessellation) {
const auto& tess = *graphics.stages.tessellation;
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology}) mix(value);
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology, tess.patchesPerGroup}) mix(value);
}
mix(graphics.rectList);
mix(programs.size());

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
#include <algorithm>
#include <array>
#include <cstdlib>
@ -6,36 +7,69 @@
namespace AgcDriver::DriverDetail {
namespace {
bool runtimeRegister(std::uint32_t offset) {
return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u;
}
const auto& registerMasks() {
static const auto masks = [] {
std::array<std::vector<std::uint64_t>, 3> bits;
for (const auto& range : Graphics::DrawKeyRegisters) {
auto& bank = bits[static_cast<std::size_t>(range.bank)];
bank.resize(std::max(bank.size(), (static_cast<std::size_t>(range.first) + range.count + 63) / 64));
for (auto offset = range.first; offset < range.first + range.count; ++offset) {
if (range.bank == Graphics::RegisterBank::Shader && runtimeRegister(offset)) continue;
bank[offset / 64] |= std::uint64_t{1} << (offset % 64);
}
}
std::array<std::shared_ptr<const Registers::Mask>, 3> result;
for (std::size_t i = 0; i < bits.size(); ++i) result[i] = std::make_shared<const Registers::Mask>(std::move(bits[i]));
return result;
}();
return masks;
}
}
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental) {
const auto& masks = registerMasks();
const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) {
const auto& mask = masks[static_cast<std::size_t>(bank)];
return incremental ? registers.Fingerprint(mask) : registers.RecomputeFingerprint(*mask);
};
auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull;
shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull;
shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull;
auto exact = shape;
const auto words = [&](std::uint32_t base, std::uint32_t count) {
for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) {
exact = (exact ^ it->first) * 0x100000001b3ull;
exact = (exact ^ it->second) * 0x100000001b3ull;
}
};
for (const auto base : {0x00cu, 0x08cu, 0x10cu}) {
const auto resources = queue.shader.find(base - 1);
const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
words(base, allUserWords ? 32u : std::min(count, 32u));
}
words(0x082u, 2);
words(0x102u, 2);
return exact;
}
std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) {
static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr;
std::uint64_t key = 0xcbf29ce484222325ull;
static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr;
auto key = RegisterStateKey(queue, allUserWords);
if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state");
const auto mix = [&](std::uint64_t value) {
key ^= value;
key *= 0x100000001b3ull;
};
const auto userEnd = [&](std::uint32_t base) {
if (allUserWords) return base + 32u;
const auto resources = queue.shader.find(base - 1);
if (resources == queue.shader.end()) return base;
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
return base + std::min(count, 32u);
};
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
const auto unread = [&](std::uint32_t offset) {
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
};
mix(deviceSerial);
for (const auto& range : Graphics::DrawKeyRegisters) {
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
mix((static_cast<std::uint64_t>(range.bank) << 32u) | range.first);
const auto end = range.first + range.count;
const bool shader = range.bank == Graphics::RegisterBank::Shader;
for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) {
if (shader && unread(it->first)) continue;
mix(it->first);
mix(it->second);
}
}
for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
const auto low = queue.shader.find(base);
const auto high = queue.shader.find(base + 1);
@ -84,7 +118,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto sameTess = [](const std::optional<ShaderRecompiler::TessellationConfiguration>& x, const std::optional<ShaderRecompiler::TessellationConfiguration>& y) {
if (x.has_value() != y.has_value()) return false;
if (!x) return true;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup;
};
if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false;
if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false;

View File

@ -14,13 +14,22 @@
namespace AgcDriver::DriverDetail {
namespace {
bool matchesBufferPatternFillKernel(std::span<const std::uint32_t> code) {
static constexpr std::array<std::uint32_t, 12> kernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u};
return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin());
}
}
bool Driver::matchesFillKernel(std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr;
if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false;
if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true;
static constexpr std::array<std::uint32_t, 9> fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u};
if (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin())) return false;
if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false;
const auto stride = (userData[1] >> 16u) & 0x3fffu;
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
@ -60,6 +69,50 @@ std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint3
return fill;
}
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
if (!enabled || userData.size() != 4 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt;
static constexpr std::array<std::uint32_t, 6> kernel{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u};
if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt;
const auto stride = (userData[1] >> 16u) & 0x3fffu;
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
const bool addTid = ((userData[3] >> 23u) & 1u) != 0;
const auto outOfBounds = (userData[3] >> 28u) & 3u;
const auto format = (userData[3] >> 12u) & 0x7fu;
const auto type = userData[3] >> 30u;
if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds != 0 || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
const auto dwords = std::min<std::uint64_t>(static_cast<std::uint64_t>(packet[1]) * 64u, userData[2]);
const auto base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt;
return DwordPatternFill{base, static_cast<std::size_t>(dwords * 4u), {}};
}
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
if (!enabled || userData.size() != 8 || packet.size() < 5 || (packet[4] & 0x20u) != 0 || !compute.groupIdEnable[0]) return std::nullopt;
const auto fill = Graphics::MatchMemoryValueFill(code, userData, compute.numThreads, {packet[1], packet[2], packet[3]});
if (!fill) return std::nullopt;
if (fill->address % 16u != 0 || fill->bytes % 16u != 0) return std::nullopt;
return fill;
}
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern) {
return std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
}
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill) {
if (!IsByteFill(pattern) || !(coversStorageKeys || memoryValueFill)) return std::nullopt;
switch (pattern[0] & 0xffu) {
case 0x00: return Graphics::DccKeys::Clear0000;
case 0x40: return Graphics::DccKeys::Clear0001;
case 0x80: return Graphics::DccKeys::Clear1110;
case 0xc0: return Graphics::DccKeys::Clear1111;
case 0x20: return Graphics::DccKeys::ClearRegister;
case 0xff: return Graphics::DccKeys::Uncompressed;
default: return std::nullopt;
}
}
bool Driver::fillClearEnabled() {
static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr;
return enabled;
@ -125,8 +178,11 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera
}
bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr<VulkanDevice>& localDevice) {
const auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false;
auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute);
std::optional<Graphics::MemoryValueFill> memoryFill;
if (!dwordFill) memoryFill = MatchMemoryValueBufferFill(packet, code, userData, compute);
if (!dwordFill && !memoryFill && !matchesFillKernel(code, userData, compute)) return false;
std::uint64_t base;
std::size_t bytes;
std::array<std::uint32_t, 4> pattern;
@ -135,6 +191,15 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
base = dwordFill->base;
bytes = dwordFill->bytes;
pattern = dwordFill->pattern;
} else if (memoryFill) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill);
inputLock.lock();
recordLabelsForPacket(localDevice.get(), queueId);
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(memoryFill->valueAddress), 4)) return false;
GuestMemory::Read(memoryFill->valueAddress, std::as_writable_bytes(std::span(pattern).first(1)), 4);
std::fill(pattern.begin() + 1, pattern.end(), pattern[0]);
base = memoryFill->address;
bytes = memoryFill->bytes;
} else if (MatchesPatternFillKernel(code, userData, compute)) {
const auto range = DecodePatternFillRange(userData, packet);
if (!range) return false;
@ -166,7 +231,16 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
const auto records = std::min<std::uint64_t>(static_cast<std::uint64_t>(groups[0]) * 64u, numRecords);
base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
bytes = static_cast<std::size_t>(records * 16u);
pattern = {userData[4], userData[5], userData[6], userData[7]};
if (matchesBufferPatternFillKernel(code)) {
const auto source = userData[4] | (static_cast<std::uint64_t>(userData[5] & 0xffffu) << 32u);
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill);
inputLock.lock();
recordLabelsForPacket(localDevice.get(), queueId);
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(source), sizeof(pattern))) return false;
GuestMemory::Read(source, std::as_writable_bytes(std::span(pattern)), 4);
} else {
pattern = {userData[4], userData[5], userData[6], userData[7]};
}
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
static std::atomic<std::uint64_t> fills{0}, filledBytes{0}, cpuFills{0};
@ -203,7 +277,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
if (traceKeys && coverage.cover == Graphics::StorageTexture::FillCover::Keys) std::fprintf(stderr, "[dcc-keys] title fills keys 0x%llx+0x%zx with %08x %08x %08x %08x (queue 0x%x)\n", static_cast<unsigned long long>(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId);
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern);
if (uniformKeysFill) {
Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
@ -236,18 +310,8 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
phase(FillFlush);
const bool stored = cleared || localDevice->FillBuffer(base, bytes, pattern);
phase(FillDevice);
if (uniformKeysFill && stored && !cleared) {
Graphics::DccKeys filled = Graphics::DccKeys::Mixed;
switch (pattern[0] & 0xffu) {
case 0x00: filled = Graphics::DccKeys::Clear0000; break;
case 0x40: filled = Graphics::DccKeys::Clear0001; break;
case 0x80: filled = Graphics::DccKeys::Clear1110; break;
case 0xc0: filled = Graphics::DccKeys::Clear1111; break;
case 0x20: filled = Graphics::DccKeys::ClearRegister; break;
case 0xff: filled = Graphics::DccKeys::Uncompressed; break;
default: break;
}
if (filled != Graphics::DccKeys::Mixed) Graphics::NoteKeysFillOnGpu(base, bytes, filled);
if (stored && !cleared) {
if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled);
}
if (profile && stored && !cleared) {
const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3];

View File

@ -205,6 +205,7 @@ void Driver::execute(const Submission& submission) {
} else if (header == FlipPacketHeader) {
CheckFailure();
std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0;
bool flipInterruptDeferred = false;
if (!drains) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
@ -212,8 +213,13 @@ void Driver::execute(const Submission& submission) {
const auto localDevice = device.Load();
if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
if (localDevice != nullptr) localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
if (localDevice != nullptr) {
localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
const auto queueId = submission.queue;
flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false);
}
}
if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
++flipsCounted;
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
flipBatchesUnsignaled += unsignaledAtFlip;

View File

@ -154,10 +154,12 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) {
const auto counts = VulkanDevice::PresentCounts();
const auto presents = counts.presents - previous.presents;
const double perPresent = presents != 0 ? 1.0 / static_cast<double>(presents) : 0.0;
const auto dropped = counts.acquireDrops - previous.acquireDrops;
const double perFrame = presents + dropped != 0 ? 1.0 / static_cast<double>(presents + dropped) : 0.0;
const auto flips = flipsCounted.load() - previousFlips;
const double perFlip = flips != 0 ? 1.0 / static_cast<double>(flips) : 0.0;
const auto serial = flipSerial.load();
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per present: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast<unsigned long long>(presents), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perPresent, (counts.gpuGapMs - previous.gpuGapMs) * perPresent, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked));
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast<unsigned long long>(presents), static_cast<unsigned long long>(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked));
previous = counts;
previousFlips = flipsCounted.load();
previousUnsignaled = flipBatchesUnsignaled.load();

View File

@ -141,6 +141,7 @@ bool RawGraphicsDrawn() {
namespace {
void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock<std::mutex>& lock) {
if (prepared.pending) PrioritizeRegistrationPreparation(prepared);
prepared.settled.wait(lock, [&] { return !prepared.pending; });
if (prepared.failure != nullptr) std::rethrow_exception(prepared.failure);
}
@ -398,6 +399,10 @@ void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler:
APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize);
return;
}
if (request.context.vertex && (request.context.vertex->paClVsOutCntl & (1u << 18u)) != 0) {
APS5_LOG_OUT_DEBUG("Shader 0x%llx is drawn into a layered target; preparing its layered variant at draw", static_cast<unsigned long long>(snapshot.codeAddress));
return;
}
if (!snapshot.header.empty()) {
if (!snapshot.prepared->deferred) APS5_LOG_ERR("Shader 0x%llx stage %u has no artifact prepared at registration for the state of this draw or dispatch; preparing it at use", static_cast<unsigned long long>(request.shader.codeAddress), static_cast<std::uint32_t>(request.shader.stage));
return;
@ -709,6 +714,7 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
plan->memory = {{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
auto& request = plan->request.emplace(ShaderRecompiler::RecompileRequest{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, plan->userData, compute, pixel, vertex, plan->memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics});
if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes;
if (graphics && graphics->tessellation) request.layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes;
if (pixel && device.GraphicsPipelineLibraries()) request.layout.pushConstantOffsetBytes = Graphics::FixedPushOffset(Stage::Fragment);
return plan;
}
@ -759,10 +765,18 @@ std::vector<PreparedShaders::Entry> PrepareRegistered(const ShaderSnapshot& snap
class RegistrationPreparePool {
public:
static RegistrationPreparePool& Instance() {
static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
static auto* pool = [] {
auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
PreparationSlot().store(created);
return created;
}();
return *pool;
}
static RegistrationPreparePool* Existing() {
return PreparationSlot().load();
}
static RegistrationPreparePool& Prebuild() {
static auto* pool = [] {
const char* text = std::getenv("APS5_PREBUILD_THREADS");
@ -778,6 +792,11 @@ public:
}
private:
static std::atomic<RegistrationPreparePool*>& PreparationSlot() {
static std::atomic<RegistrationPreparePool*> slot{nullptr};
return slot;
}
static std::atomic<RegistrationPreparePool*>& PrebuildSlot() {
static std::atomic<RegistrationPreparePool*> slot{nullptr};
return slot;
@ -785,11 +804,12 @@ private:
public:
void Submit(std::function<void()> task) {
void Submit(std::function<void()> task, const PreparedShaders* prepared = nullptr) {
{
std::unique_lock lock(mutex);
if (threads != 0 && !closed) {
tasks.push_back(std::move(task));
if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared});
else tasks.push_back({std::move(task), prepared});
lock.unlock();
ready.notify_one();
return;
@ -798,6 +818,14 @@ public:
task();
}
void Prioritize(const PreparedShaders& prepared) {
std::lock_guard lock(mutex);
const auto found = std::ranges::find_if(tasks, [&](const auto& task) { return task.prepared == &prepared; });
if (found == tasks.end()) return;
if (std::getenv("APS5_TRACE_PREPARATION_WAIT") != nullptr) std::fprintf(stderr, "[shader-prepare] demanded queued task at position %zu of %zu\n", static_cast<std::size_t>(std::distance(tasks.begin(), found)), tasks.size());
std::rotate(tasks.begin(), found, std::next(found));
}
void Close() {
std::unique_lock lock(mutex);
closed = true;
@ -823,7 +851,7 @@ private:
auto& pool = *static_cast<RegistrationPreparePool*>(argument);
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8);
for (;;) {
std::function<void()> task;
Task task;
{
std::unique_lock lock(pool.mutex);
pool.ready.wait(lock, [&] { return !pool.tasks.empty(); });
@ -831,8 +859,8 @@ private:
pool.tasks.pop_front();
++pool.running;
}
task();
task = nullptr;
task.function();
task = {};
{
std::lock_guard lock(pool.mutex);
--pool.running;
@ -842,10 +870,15 @@ private:
return nullptr;
}
struct Task {
std::function<void()> function;
const PreparedShaders* prepared = nullptr;
};
std::mutex mutex;
std::condition_variable ready;
std::condition_variable idle;
std::deque<std::function<void()>> tasks;
std::deque<Task> tasks;
unsigned threads = 0;
unsigned running = 0;
bool closed = false;
@ -857,6 +890,7 @@ bool AsyncRegistrationPrepare() {
}
void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapshot, std::shared_ptr<RegisteredPreparation> plan) {
const auto* preparedKey = snapshot->prepared.get();
SubmitRegistrationPreparation([snapshot = std::move(snapshot), plan = std::move(plan)] {
PerformanceContext timingContext(FrameTiming::Preparation());
std::vector<PreparedShaders::Entry> entries;
@ -878,13 +912,18 @@ void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapsho
prepared.pending = false;
}
prepared.settled.notify_all();
});
}, preparedKey);
}
}
void SubmitRegistrationPreparation(std::function<void()> task) {
RegistrationPreparePool::Instance().Submit(std::move(task));
void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared) {
RegistrationPreparePool::Instance().Submit(std::move(task), prepared);
}
void PrioritizeRegistrationPreparation(PreparedShaders& prepared) {
prepared.demanded.store(true);
if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared);
}
void SubmitLibraryPrebuild(std::function<void()> task) {
@ -923,7 +962,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
std::vector<PreparedGraphicsStage> prepared;
std::uint32_t pushOffset = 0;
const bool fixedSlots = target.fixedPushSlots;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes;
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue;
const auto& program = decoded.programs[index];
@ -931,7 +970,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize;
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr);
if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl);
if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl) | decoded.state.layerExports;
pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots);
const auto offset = pushOffset;
const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes;

View File

@ -1212,7 +1212,14 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
saved.assign(source, source + (stop - first));
}
const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true);
if (!import()) return false;
bool imported = false;
{
#ifdef __APPLE__
const GuestArena::HostWrite open(reinterpret_cast<void*>(first), static_cast<std::size_t>(stop - first));
#endif
imported = import();
}
if (!imported) return false;
if (!staysWatched) {
if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast<std::size_t>(stop - first));
unwatchLocked(tracker, address, bytes);
@ -1263,6 +1270,30 @@ bool UnchangedSinceAll(std::span<const UnchangedQuery> queries) {
return true;
}
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges) {
ranges.clear();
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address || !WriteWatched()) return 0;
auto committed = CommittedRanges(address, bytes);
auto generation = TrackerGeneration();
for (const auto& [begin, end] : committed) {
generation = CollectWrites(begin, static_cast<std::size_t>(end - begin));
if (generation == 0) return 0;
}
ranges = std::move(committed);
return generation;
}
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges, std::uint64_t generation) {
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
tracker.initialize();
if (!tracker.watched || generation == 0) return false;
for (const auto& [begin, end] : ranges) {
if (end <= begin || !tracker.covers(begin, static_cast<std::size_t>(end - begin)) || tracker.newestStamp(begin, end) > generation) return false;
}
return true;
}
std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function<std::pair<std::uint64_t, std::uint64_t>()>& store) {
if (bytes == 0) return 0;
const auto stampStored = [](WriteTracker& tracker, std::pair<std::uint64_t, std::uint64_t> stored) -> std::uint64_t {

View File

@ -94,7 +94,7 @@ bool CoreVulkanCapability(std::uint32_t capability) {
}
}
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) {
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) {
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.flags = flags;
info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
@ -107,6 +107,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create,
subgroup.requiredSubgroupSize = subgroupSize;
if (subgroupSize != 0) info.stage.pNext = &subgroup;
info.layout = layout;
auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{};
check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines");
}
@ -273,6 +274,7 @@ struct VulkanDevice::State {
bool shaderClock = false;
bool narrowSubgroupClock = false;
bool float64DenormPreserve = false;
bool float16DenormPreserve = false;
// VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in
// graphics stages, and compute workgroups wider than a wave).
bool descriptorIndexing = false;
@ -283,6 +285,7 @@ struct VulkanDevice::State {
VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT};
bool imageInt64Atomics = false;
bool bufferInt64Atomics = false;
bool viewportIndexLayer = false;
bool primitiveListRestart = false;
bool depthClipControl = false;
bool graphicsPipelineLibrary = false;
@ -1149,6 +1152,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->imageInt64Atomics = true;
state->spirvExtensions.push_back("SPV_EXT_shader_image_int64");
}
if (hasExtension(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME)) {
deviceExtensions.push_back(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityShaderViewportIndexLayerEXT);
state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer");
state->viewportIndexLayer = true;
}
deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve);
state->spirvExtensions.push_back("SPV_KHR_float_controls");
@ -1167,6 +1176,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->capabilities.push_back(spv::CapabilityRoundingModeRTE);
state->capabilities.push_back(spv::CapabilityDenormPreserve);
}
state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE &&
floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL;
if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve);
deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME);
state->capabilities.push_back(4448);
@ -2165,7 +2177,7 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st
// a completion-deferred label): it lands only when that batch is reaped, whatever its fence.
const auto completionStoreOverlaps = [&](std::uint64_t address) {
if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true;
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.PendingLabelIn(address, bytes);
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes);
};
// The CPU decision (see the header): pure queries, in the order that makes the first failing
// test the reason; a signaled writer of the source is fine (its in-place stores are final in
@ -2558,9 +2570,23 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP"));
if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip");
else Graphics::StorageTexture::FlushDumpSelected("flip");
auto& dumps = Dumps();
if (buffer.tilingMode == 1) {
const auto pixels = ReadDisplayBuffer(buffer);
return present(buffer.width, buffer.height, true, pixels);
bool dumpFrame = false;
if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
if (dumps.cpu) {
WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale());
} else {
state->nextDumpIndex = dumps.dumped++;
dumpFrame = true;
}
}
if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame)) {
if (dumpFrame) --dumps.dumped;
return false;
}
return true;
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
// The display buffer is usually a resident render target whose results are still on the GPU: it
@ -2599,7 +2625,6 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
}
}
const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt;
auto& dumps = Dumps();
bool dumpFrame = false;
if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
if (dumps.cpu) {
@ -2642,11 +2667,15 @@ bool VulkanDevice::AcquireImage() {
std::uint32_t index = 0;
timing.Mark("fence_reset");
// An out-of-date swapchain (the window changed) drops this frame; the next Resize recreates it.
const auto acquired = state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 5'000'000'000ULL, VK_NULL_HANDLE, state->acquireFence, &index);
const auto acquired = state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 0, VK_NULL_HANDLE, state->acquireFence, &index);
if (acquired == VK_ERROR_OUT_OF_DATE_KHR) {
state->extent = {0, 0};
return false;
}
if (acquired == VK_NOT_READY || acquired == VK_TIMEOUT) {
++presentCounters.acquireDrops;
return false;
}
if (acquired != VK_SUBOPTIMAL_KHR) check(acquired, "vkAcquireNextImageKHR");
timing.Mark("acquire_image");
APS5_LOG_OUT_DEBUG("vkAcquireNextImageKHR index=%u imageCount=%zu", index, state->images.size());
@ -3023,6 +3052,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
target.nonConstantImageOffsets = state->maintenance8;
target.narrowSubgroupClock = state->narrowSubgroupClock;
target.float64DenormPreserve = state->float64DenormPreserve;
target.float16DenormPreserve = state->float16DenormPreserve;
target.fixedPushSlots = state->graphicsPipelineLibrary;
target.registrationModules = state->graphicsPipelineLibrary;
target.srgbDecodeFormats = state->srgbDecodeFormats;
@ -3104,6 +3134,7 @@ Graphics::Context VulkanDevice::buildContext() const {
state->depthClamp,
state->hostImportAlignment
};
context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr;
context.dmaBufImport = state->dmaBufImport;
context.recorder = state->recorder.get();
context.descriptorCache = state->descriptorCache.get();
@ -3136,6 +3167,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors();
context.bufferInt64Atomics = state->bufferInt64Atomics;
context.placeholderImages = state->placeholderImages.get();
context.viewportIndexLayer = state->viewportIndexLayer;
context.primitiveListRestart = state->primitiveListRestart;
context.imageViewMinLod = state->imageViewMinLod;
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
@ -4258,7 +4290,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str());
}
if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast<unsigned long long>(programAddress));
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline);
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization);
objects->destroyModule(objects->device, objects->module, nullptr);
objects->module = VK_NULL_HANDLE;
Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline);

View File

@ -9,6 +9,8 @@
#include <cstdint>
#include <cstdio>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <stdexcept>
#include <string>
#include <vector>
@ -28,6 +30,19 @@ class SamplerCache;
class ShaderResources;
class PlaceholderImages;
class PipelineCacheSynchronization {
public:
using PipelineUseLock = std::shared_lock<std::shared_mutex>;
using SnapshotLock = std::unique_lock<std::shared_mutex>;
PipelineUseLock LockForPipelineUse() const { return PipelineUseLock(mutex); }
SnapshotLock LockForSnapshot() const { return SnapshotLock(mutex); }
SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); }
private:
mutable std::shared_mutex mutex;
};
inline void Require(bool condition, const std::string& reason) {
if (!condition) throw std::runtime_error("AGC graphics: " + reason);
}
@ -165,6 +180,7 @@ struct Context {
bool nullDescriptors = false;
bool bufferInt64Atomics = false;
const PlaceholderImages* placeholderImages = nullptr;
bool viewportIndexLayer = false;
bool primitiveListRestart = false;
bool imageViewMinLod = false;
bool pipelineExecutableInfo = false;
@ -175,6 +191,11 @@ struct Context {
bool provokingVertexModePerPipeline = false;
bool singlePassStorage = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr;
PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const {
return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
}
template<typename TFunction>
TFunction Function(const char* name) const {

View File

@ -33,7 +33,9 @@ struct HostImport {
std::uint64_t serial = 0;
bool unwatched = false;
bool dmaBuf = false;
bool joined = false;
std::shared_ptr<void> chunk;
mutable std::uint64_t lastUse = 0;
};
enum class ImportWatch : std::uint8_t { Watch, Unwatch };
@ -58,6 +60,10 @@ void SetImportWatch(const Context& context, ImportWatch watch);
// the import's buffer.
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
void SetHostImportWindow(std::uint64_t bytes);
// Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one).
void SetHostImportRangeWindow(std::uint64_t bytes);
void SetHostImportBudget(std::uint64_t bytes);
std::uint64_t HostImportLiveBytes();
bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice);
// Whether an existing import covers [address, address + bytes), without reconciling the imports
// with the registry or making one (HostImportFor may take a registry lease): a hint for choices
@ -378,6 +384,7 @@ private:
// Import registry epoch when `direct` pointers were taken at acquire time; they are reused while
// no import was destroyed since.
std::uint64_t importsEpoch = 0;
std::uint64_t importFloor = 0;
std::vector<Region> regions;
// Whether `regions` is in ascending address order (true right after AcquireRegistered, whose
// regions follow the registry's order), so AddSnapshot can search instead of scanning.

View File

@ -15,7 +15,7 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel
// the batch completes, so a recorded draw keeps the object (Recorder::Keep) like its pipeline.
class Framebuffer {
public:
Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent);
Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent, std::uint32_t layers = 1);
~Framebuffer();
Framebuffer(const Framebuffer&) = delete;
Framebuffer& operator=(const Framebuffer&) = delete;
@ -47,7 +47,7 @@ public:
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
VkPipelineLayout Layout() const;
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent = {});
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers = 1, std::span<const std::uint8_t> persistent = {});
// Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state.
void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const;
// The same inside a render pass another pipeline of the same attachments began (compatible by
@ -68,6 +68,7 @@ private:
std::vector<std::weak_ptr<StorageTexture>> owners;
std::vector<bool> owned;
VkExtent2D extent;
std::uint32_t layers = 1;
std::shared_ptr<Framebuffer> framebuffer;
};
void release() noexcept;
@ -123,7 +124,7 @@ bool ValidateSampleLocations(const Context& context, const State& state);
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
bool ShadersWriteMemory(std::span<const CompiledShader> shaders);
// Returns the color attachment locations the pixel shader writes.
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false);
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false);
}

View File

@ -21,6 +21,7 @@ public:
PipelineCache(const PipelineCache&) = delete;
PipelineCache& operator=(const PipelineCache&) = delete;
VkPipelineCache Handle() const { return cache; }
PipelineCacheSynchronization& Synchronization() { return synchronization; }
private:
void load(std::vector<std::byte>& initialData);
@ -30,6 +31,7 @@ private:
Context context;
VkPhysicalDeviceProperties properties{};
VkPipelineCache cache = VK_NULL_HANDLE;
PipelineCacheSynchronization synchronization;
std::filesystem::path path;
std::size_t savedBytes = 0;
std::mutex mutex;

View File

@ -66,7 +66,7 @@ private:
};
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);

View File

@ -38,6 +38,7 @@ TextureCacheUse TextureCacheUsage();
std::uint64_t SampledTextureCacheBudget(const Context& context);
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words);
bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<const std::uint32_t> second);
void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes);
// The cached storage image of a surface (render targets use it as their resident image); brought up
// to date with guest memory before it is returned.
@ -432,6 +433,7 @@ private:
Context context;
std::vector<std::uint32_t> layoutKey;
GuestBufferMemory guestMemory;
std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes();
std::unique_ptr<BdaResources> bda;
bool usesBda = false;
bool usesFaultBuffer = false;
@ -465,6 +467,7 @@ private:
std::vector<std::shared_ptr<Texture>> textures;
std::vector<bool> textureFirstLayer;
std::vector<bool> textureBaseLevel;
std::vector<bool> textureSrgb;
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
std::vector<std::uint32_t> storageMips;
std::vector<std::uint64_t> storageKeys;

View File

@ -73,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span<const CompiledShader> sha
VkShaderStageFlags result = 0;
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh) result |= VulkanStage(shader.stage);
if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage);
}
return result;
}

View File

@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <vector>
#include <array>
@ -28,6 +29,8 @@ struct ShaderStages {
std::uint32_t fragmentWaveSize;
std::optional<ShaderRecompiler::MeshConfiguration> mesh;
std::optional<ShaderRecompiler::TessellationConfiguration> tessellation;
float minTessellationLevel = 1.0f;
float maxTessellationLevel = 64.0f;
};
struct ColorTarget {
@ -58,6 +61,7 @@ struct ColorTarget {
std::uint64_t arrayAddress = 0;
std::uint32_t arrayLastSlice = 0;
std::uint32_t arraySlice = 0;
std::uint32_t layers = 1;
std::uint32_t exportIndex = 0;
bool uintExport = false;
std::uint32_t pipeBankXor = 0;
@ -102,6 +106,8 @@ struct State {
bool rectList = false;
bool dualSourceBlend = false;
VkExtent2D renderExtent;
std::uint32_t renderLayers = 1;
std::uint32_t layerExports = 0;
VkPrimitiveTopology topology;
bool primitiveRestart = false;
VkViewport viewport;
@ -134,6 +140,7 @@ ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state);
ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot);
std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height);
std::uint32_t ColorWriteMask(const Registers& context);
GuestTextureResource SurfaceForTarget(const ColorTarget& color);
struct ColorMetadataPass {
enum class Mode { EliminateFastClear, DccDecompress, Resolve };

View File

@ -66,6 +66,9 @@ public:
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag);
static VkFormat DepthCopyFormat(VkFormat colorFormat);
static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
struct BlockCopyTag {};
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag);
static bool CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; }
bool RefreshDepthCopy();
struct DepthCopyCounts {
@ -81,6 +84,8 @@ public:
VkImageView View() const;
VkImageView FirstLayerView() const { return firstLayerView; }
VkImageView BaseLevelView() const;
bool HasSrgbView() const { return srgbInfo.has_value(); }
VkImageView SrgbView(bool firstLayer, bool baseLevel) const;
struct ViewRange {
VkImageViewType type;
std::uint32_t levels;
@ -106,8 +111,10 @@ public:
private:
void release() noexcept;
bool refreshBlockCopy();
void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
// Held by value: cached textures outlive the Context of the draw that created them.
Context context;
@ -122,6 +129,10 @@ private:
std::optional<VkImageViewCreateInfo> baseLevelInfo;
mutable std::mutex baseLevelMutex;
mutable VkImageView baseLevelView = VK_NULL_HANDLE;
std::optional<VkImageViewCreateInfo> srgbInfo;
float srgbMinLod = 0.0f;
mutable std::mutex srgbMutex;
mutable std::array<VkImageView, 3> srgbViews{};
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDeviceSize allocationBytes = 0;
VkDeviceSize countedBytes = 0;
@ -133,10 +144,12 @@ private:
bool copiedOnce = false;
std::uint64_t copiedVersion = 0;
VkFormat depthFormat = VK_FORMAT_UNDEFINED;
VkFormat blockFormat = VK_FORMAT_UNDEFINED;
std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0;
};
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format);
VkFormat SrgbTwinFormat(const Context& context, VkFormat format);
// A guest texture a shader writes through a storage image. It is uploaded like a sampled texture;
// after the GPU work completes its results are stored to guest memory (retiled, changed bytes only),
@ -162,7 +175,7 @@ public:
// Render targets live in the same images: draws attach mip 0 through a view of the color
// buffer's format and mark the image dirty like a storage write.
bool Attachable() const { return attachable; }
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layer = 0);
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layers = 1, std::uint32_t layer = 0);
VkImageView AttachmentProxyView();
void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
@ -316,7 +329,7 @@ public:
// GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A.
DccKeys UploadedKeys() const { return uploadedKeys; }
DccKeys FilledKeys() const { return filledKeys; }
bool GuestSnapshotValid() const { return originalValid; }
bool GuestSnapshotValid() const { return originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)); }
DccKeyProof& KeyProof() const { return keyProof; }
DccRangeProof& TargetKeyProof() const { return targetKeyProof; }
DccKeys ProvedKeys() const;
@ -328,6 +341,7 @@ public:
static std::uint64_t SinglePassMoves();
static std::uint64_t RefreshesProved();
std::uint64_t GuestBytes() const;
std::size_t OriginalSnapshotBytes() const { return original.capacity(); }
VkDeviceSize AllocationBytes() const { return memoryBytes; }
private:
@ -338,6 +352,7 @@ private:
// upload everything).
void upload(const std::vector<bool>* layers = nullptr);
void captureGuestBytes(const std::vector<bool>* layers);
bool originalHolds(std::uint64_t address, std::size_t bytes) const;
bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize = false) const;
// Stores the pending tracked layers overlapping [address, address + bytes) to guest memory; in
// each, 64 KiB blocks the CPU wrote since the layer's generation keep the CPU's bytes. Block
@ -535,7 +550,7 @@ private:
std::map<std::uint32_t, VkImageView> elementLayerViews;
std::int8_t singlePassState = -1;
bool attachable = false;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
VkImage proxyImage = VK_NULL_HANDLE;
VkDeviceMemory proxyMemory = VK_NULL_HANDLE;
VkImageView proxyView = VK_NULL_HANDLE;
@ -544,7 +559,7 @@ private:
bool dirty = false;
std::uint64_t version = 0;
// `original` holds the guest bytes; false after a GPU-side clear, which never read them.
bool originalValid = true;
std::vector<bool> layerOriginal;
// The storage cache let the image go (Flush): a lookup makes a new image of the surface, so
// no fill may put results into this one (guarded by the live-image registry lock).
bool released = false;

View File

@ -180,6 +180,13 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute&
return static_cast<std::size_t>(required);
}
inline std::size_t VertexBufferReadOffset(const ShaderRecompiler::VertexAttribute& attribute, std::uint32_t firstVertex, std::uint32_t firstInstance) {
Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index");
const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu;
const auto index = attribute.fetchIndex == 0 ? firstVertex : firstInstance;
return static_cast<std::size_t>(static_cast<std::uint64_t>(stride) * index);
}
struct VertexFetch {
std::uint64_t begin;
std::uint64_t end;

View File

@ -29,10 +29,10 @@ void main() {
uint nibble = ((y / 512u) * push.blocksPerRow + x / 1024u) * 8192u + offset;
uint byteAddress = push.base + nibble / 2u;
uint code = (cmask.words[byteAddress / 4u] >> ((byteAddress % 4u) * 8u + (nibble % 2u) * 4u)) & 0xfu;
if (code == 0xfu) return;
if (code == 0u && push.write != 0u) {
if (code != 0u) return;
if (push.write != 0u) {
imageStore(target, ivec2(pixel), uvec4(push.clear0, push.clear1, 0u, 0u));
return;
}
if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u << code);
if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u);
}

View File

@ -356,6 +356,7 @@ bool LayoutFor(VkFormat format, Layout& layout) {
case VK_FORMAT_R16G16_UINT: layout = {2, {16, 16}, Kind::Uint}; return true;
case VK_FORMAT_R16G16_SINT: layout = {2, {16, 16}, Kind::Sint}; return true;
case VK_FORMAT_R16G16_SFLOAT: layout = {2, {16, 16}, Kind::Float}; return true;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: layout = {3, {11, 11, 10}, Kind::Float}; return true;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: case VK_FORMAT_A2R10G10B10_UNORM_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Unorm}; return true;
case VK_FORMAT_A2B10G10R10_UINT_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Uint}; return true;
case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: layout = {4, {8, 8, 8, 8}, Kind::Unorm}; return true;
@ -382,7 +383,14 @@ std::uint64_t One(Kind kind, std::uint32_t bits) {
switch (kind) {
case Kind::Unorm: case Kind::Uint: return bits >= 64 ? ~0ull : (1ull << bits) - 1u;
case Kind::Snorm: case Kind::Sint: return (1ull << (bits - 1u)) - 1u;
case Kind::Float: return bits == 16 ? 0x3c00u : 0x3f800000u;
case Kind::Float:
switch (bits) {
case 10: return 0x1e0u;
case 11: return 0x3c0u;
case 16: return 0x3c00u;
case 32: return 0x3f800000u;
default: throw std::runtime_error("AGC graphics: DCC clear of a " + std::to_string(bits) + "-bit float channel is not implemented");
}
}
return 0;
}
@ -460,7 +468,9 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized
DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) {
memoized = false;
if (resource.dccAddress == 0) return DccKeys::Uncompressed;
const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
DccKeys keys;
if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known;
else keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
if (keys == DccKeys::Uncompressed || (keys == DccKeys::ClearSingle && SingleKeysReadable(resource))) return keys;
if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented");
std::byte probe[16]{};

View File

@ -153,18 +153,17 @@ public:
float clearDepth = 0.0f;
std::uint8_t clearStencil = 0;
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr;
bool StorageViewMatches(std::span<const std::uint32_t> words, const GuestTextureResource& resource) const {
if (resource.width != target.extent.width || resource.height != target.extent.height) return false;
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u);
if (ResolveTextureFormat(resource.format) != expected && !depthBits) return nullptr;
if (resource.dimension != TextureDimension::k2D) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
throw std::runtime_error(text);
}
return ResolveTextureFormat(resource.format) == expected || depthBits;
}
void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const {
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented");
if (!stencil && hostFormat != target.format) {
char text[224];
@ -176,9 +175,21 @@ public:
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast<unsigned long long>(target.address));
throw std::runtime_error(text);
}
}
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
if (!StorageViewMatches(words, resource)) return nullptr;
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0;
if (resource.dimension != TextureDimension::k2D && !oneLayer) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
throw std::runtime_error(text);
}
RequireStorable(resource, mip);
ApplyFastClear();
auto& plane = stencil ? stencilPlane : depthPlane;
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress) {
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) {
std::erase_if(plane.replaced, [](const auto& old) { return old.expired(); });
if (plane.storage != nullptr) plane.replaced.push_back(plane.storage);
plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
@ -438,6 +449,82 @@ std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, VkFor
return *copies;
}
struct LayeredSlice {
DepthSurface* surface;
std::uint32_t layer;
};
class LayeredDepthStorage {
public:
LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), layers(layers) {
const auto bytes = DescribeSurface(resource).guestBytes;
StorageTexture::FlushPending(resource.baseAddress, static_cast<std::size_t>(bytes), nullptr, "storage image creation", PublishScope::None);
storage = StorageTexture::Create(context, *context.detiler, resource, 0);
Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers");
staging = std::make_unique<DeviceBuffer>(context, static_cast<std::size_t>(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
}
void Copy(std::span<const LayeredSlice> slices, bool toStorage) {
if (slices.empty()) return;
auto* recorder = Recorder::Active();
Require(recorder != nullptr, "depth surface storage copies need an active recorder");
const auto commands = recorder->Commands();
recorder->Keep(storage);
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy);
std::vector<VkBufferImageCopy> storageRegions;
const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspect, std::uint32_t imageLayer) {
VkBufferImageCopy copy{};
copy.bufferOffset = sliceBytes() * layer;
copy.imageSubresource = {aspect, 0, imageLayer, 1};
copy.imageExtent = {extent.width, extent.height, 1};
return copy;
};
for (const auto& slice : slices) storageRegions.push_back(region(slice.layer, VK_IMAGE_ASPECT_COLOR_BIT, slice.layer));
const auto copyToBuffer = context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer");
const auto copyToImage = context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage");
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
if (toStorage) {
for (const auto& slice : slices) {
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
copyToBuffer(commands, slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, &copy);
}
} else {
copyToBuffer(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
}
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
if (toStorage) {
copyToImage(commands, staging->Handle(), storage->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
} else {
for (const auto& slice : slices) {
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
copyToImage(commands, staging->Handle(), slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, 1, &copy);
}
}
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
recorder->EndGpuTiming(timing, sliceBytes() * slices.size());
}
const Context context;
const DepthTarget base;
const VkExtent2D extent;
const std::uint32_t bytesPerTexel;
const std::uint32_t layers;
std::shared_ptr<StorageTexture> storage;
private:
std::unique_ptr<DeviceBuffer> staging;
VkDeviceSize sliceBytes() const {
return static_cast<VkDeviceSize>(extent.width) * extent.height * bytesPerTexel;
}
};
std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>& layeredStorages() {
static auto* storages = new std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>();
return *storages;
}
bool sameSurface(const DepthTarget& a, const DepthTarget& b) {
return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples;
}
@ -574,6 +661,7 @@ void ClearDepthSurfaces(VkDevice device) {
drawnDepth = nullptr;
std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; });
std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
}
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
@ -681,17 +769,71 @@ bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words
return ResolveTextureFormat(resource.format) == expected || depthBits;
}
namespace {
std::vector<LayeredSlice> layeredSlices(const Context& context, const LayeredDepthStorage& storage) {
const auto& base = storage.base;
std::vector<LayeredSlice> slices;
const auto& list = surfaces();
for (std::uint32_t layer = 0; layer < storage.layers; ++layer) {
const auto address = base.address + static_cast<std::uint64_t>(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel);
const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; });
if (slice != list.rend()) slices.push_back({slice->get(), layer});
}
return slices;
}
std::shared_ptr<StorageTexture> layeredDepthStorage(const Context& context, DepthSurface& surface, const GuestTextureResource& resource, std::uint32_t mip) {
const auto& target = surface.target;
const auto layers = resource.depthOrLastArray + 1u;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytesPerTexel = d16 ? 2u : 4u;
if (resource.baseArray != 0) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx through a 2D array view of slices %u-%u is not implemented", static_cast<unsigned long long>(target.address), resource.baseArray, resource.depthOrLastArray);
throw std::runtime_error(text);
}
surface.RequireStorable(resource, mip);
const auto geometry = DescribeSurface(resource);
if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast<unsigned long long>(target.address), static_cast<unsigned long long>(geometry.GuestLayerOffset(1)), static_cast<unsigned long long>(DepthSliceBytes(target.extent, bytesPerTexel)));
throw std::runtime_error(text);
}
auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast<int>(resource.tileMode), static_cast<int>(resource.dimension)}];
if (entry == nullptr) entry = std::make_unique<LayeredDepthStorage>(context, resource, target, bytesPerTexel, layers);
else static_cast<void>(entry->storage->Refresh());
const auto slices = layeredSlices(context, *entry);
for (const auto& slice : slices) slice.surface->ApplyFastClear();
entry->Copy(slices, true);
return entry->storage;
}
}
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
std::lock_guard lock(surfacesMutex());
const auto& list = surfaces();
const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) {
return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress));
});
return found == list.rend() ? nullptr : (*found)->Storage(words, resource, mip);
if (found == list.rend()) return nullptr;
auto& surface = **found;
const bool stencil = surface.target.stencilAddress != 0 && surface.target.stencilAddress == resource.baseAddress;
if (stencil || resource.dimension != TextureDimension::k2DArray || (resource.baseArray == 0 && resource.depthOrLastArray == 0)) return surface.Storage(words, resource, mip);
if (!surface.StorageViewMatches(words, resource)) return nullptr;
return layeredDepthStorage(context, surface, resource, mip);
}
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) {
std::lock_guard lock(surfacesMutex());
for (auto& [key, entry] : layeredStorages()) {
if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue;
const auto slices = layeredSlices(context, *entry);
entry->Copy(slices, false);
for (const auto& slice : slices) slice.surface->written = ++depthWrites;
return true;
}
for (const auto& surface : surfaces()) {
if (surface->context.device == context.device && surface->StoreBack(storage)) return true;
}

View File

@ -61,38 +61,6 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std:
namespace {
std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest;
throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast<int>(format)));
}
// The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X).
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident");
const bool chain = color.mipCount > 1;
GuestTextureResource surface{};
surface.baseAddress = chain ? color.surfaceAddress : color.address;
surface.pipeBankXor = color.pipeBankXor;
surface.width = chain ? color.surfaceExtent.width : color.extent.width;
surface.height = chain ? color.surfaceExtent.height : color.extent.height;
surface.depthOrLastArray = color.depth - 1u;
surface.baseArray = 0;
surface.mipCount = color.mipCount;
surface.baseLevel = 0;
surface.lastLevel = color.mipCount - 1;
surface.tileMode = ColorTextureTileMode(color.tileMode);
surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D;
surface.format = GuestFormatFor(color.format, color.elementBytes);
surface.dstSelX = 4;
surface.dstSelY = 5;
surface.dstSelZ = 6;
surface.dstSelW = 7;
surface.dccAddress = color.dccAddress;
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
surface.dccPipeAligned = color.dccPipeAligned;
return surface;
}
GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) {
Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array");
auto surface = SurfaceForTarget(color);
@ -221,20 +189,8 @@ void proveCmask(std::uint64_t address, std::size_t bytes, std::uint64_t generati
else cmaskProofs()[address] = {bytes, generation};
}
std::string hexAddress(std::uint64_t address) {
char text[24];
std::snprintf(text, sizeof(text), "0x%llx", static_cast<unsigned long long>(address));
return text;
}
void requireCmaskCodes(std::uint32_t codes) {
if (codes == 0) return;
Require((codes & 1u) == 0, "a CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys, was recorded");
std::string listed;
for (std::uint32_t code = 1; code < 15; ++code) {
if ((codes >> code) & 1u) listed += (listed.empty() ? "" : ", ") + std::to_string(code);
}
Require(false, "CMASK codes " + listed + " were recorded, which are not a single-sample fast-clear code (0 cleared, 15 expanded)");
}
CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::vector<std::uint32_t>& cleared) {
@ -264,7 +220,6 @@ CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::v
const auto nibble = layout.Nibble(tileX, tileY);
const auto code = (std::to_integer<std::uint32_t>(mask[nibble / 2u]) >> ((nibble % 2u) * 4u)) & 0xfu;
if (code == 0u) cleared.push_back(tileY * layout.TilesX() + tileX);
else Require(code == 0xfu, "CMASK code " + std::to_string(code) + " of color target " + hexAddress(color.address) + " is not a single-sample fast-clear code (0 cleared, 15 expanded)");
}
}
if (cleared.empty()) return CmaskState::Expanded;
@ -688,6 +643,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(context.fragmentShaderBarycentric);
add(context.shaderClipDistance);
add(context.shaderCullDistance);
add(context.viewportIndexLayer);
add(state.paClVsOutCntl);
add(state.stages.mesh.has_value());
if (state.stages.mesh) {
@ -709,6 +665,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(tessellation.domain);
add(tessellation.partitioning);
add(tessellation.outputTopology);
add(tessellation.patchesPerGroup);
}
}
return keyed;
@ -740,7 +697,7 @@ std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<
}
std::set<std::uint32_t> outputs;
try {
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance);
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer);
} catch (const std::exception& error) {
if (keyed) {
std::lock_guard lock(validationMutex());
@ -1080,6 +1037,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
const auto& mesh = *state.stages.mesh;
const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u;
Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive");
Require(((state.stages.registerValue >> 19u) & 3u) == 0 || !draw.indexed, "indexed fast-launch draws are unsupported");
if (args == nullptr) {
const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize;
const auto primitives = (draw.indexCount - inputSize) / step + 1u;
@ -1179,7 +1137,8 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
const auto bytes = args != nullptr ? VertexBufferExtent(attribute) : VertexBufferReadSize(attribute, inputs.maxIndex, draw.instanceCount, draw.firstInstance);
const auto& fields = attribute.resource.fields;
const auto address = fields[0] | (static_cast<std::uint64_t>(fields[1] & 0xffffu) << 32u);
Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address, "vertex buffer aliases the render target");
const auto firstByte = args == nullptr ? VertexBufferReadOffset(attribute, draw.firstVertex, draw.firstInstance) : 0;
Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address + firstByte, "vertex buffer aliases the render target");
const auto stride = (fields[1] >> 16u) & 0x3fffu;
fetchOf[i] = fetches.size();
fetches.push_back({address, address + bytes, stride, attribute.fetchIndex, DecodeVertexFormat(attribute).alignment});
@ -1590,6 +1549,12 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const
resources.PatchPushConstants(block);
stages = PushConstantStages(shaders);
}
if (state.stages.tessellation) {
for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || shader.pushConstantOffset >= PipelinePushSlotBytes || shader.pushConstantOffset + shader.program->pushConstants.size() <= ShaderRecompiler::TessellationLevelPushOffsetBytes, "a stage's push constants overlap the tessellation levels");
const std::array<float, ShaderRecompiler::TessellationLevelPushBytes / 4> levels{state.stages.minTessellationLevel, state.stages.maxTessellationLevel};
static_assert(ShaderRecompiler::TessellationLevelPushOffsetBytes + ShaderRecompiler::TessellationLevelPushBytes == PipelinePushSlotBytes);
std::memcpy(block.data() + ShaderRecompiler::TessellationLevelPushOffsetBytes, levels.data(), sizeof(levels));
}
if (!state.stages.mesh) {
pipeline.PushConstants(commands, stages, block);
return;
@ -1968,7 +1933,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (binding.resident != nullptr) {
timer.phase(PhaseReadTarget);
binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED;
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.arraySlice));
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.layers, color.arraySlice));
continue;
}
materializeCmaskClear(context, color, nullptr);
@ -2156,7 +2121,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
owners.push_back(binding.resident);
persistent.push_back(binding.multisampled ? 1u : 0u);
}
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent);
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, state.renderLayers, persistent);
timer.phase(PhasePipeline);
APS5_LOG_CHARS_OUT_DEBUG("Pipeline created");
if (lean) {
@ -2180,7 +2145,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// only a template the resource cache serves under this content key (reusable: no lease,
// no copied writes, every direct region import- or mirror-served) of a direct draw, so a
// hit's proof is the template's ProveCurrent and nothing needs completion work.
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) {
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) {
auto recipe = std::make_shared<DrawRecipe>();
recipe->device = context.device;
recipe->templateRef = resources;
@ -2199,6 +2164,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
recipe->masked = masked;
recipe->fragmentOutputs = inputs.fragmentOutputs;
recipe->shaderStages = inputs.shaderStages;
recipe->hasDepth = state.depth.has_value();
*recipeOut = std::move(recipe);
}
timing.Mark("draw_and_resource_release");
@ -2476,7 +2442,9 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
Require(!draw.indirect, "a draw recipe covers direct draws only");
auto* recorder = Recorder::Active();
if (!RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable);
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == state.colors.size(), "draw recipe targets do not match the state");
if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone);
const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u);
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state");
auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe);
if (inputs.nothing) {
result.recorded = true;
@ -2502,6 +2470,12 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
if (resident == nullptr) return miss(DrawRecipeMiss::TargetGone);
targets.push_back(std::move(resident));
}
if (recipe.hasDepth) {
timer.phase(PhaseSetup);
const auto depthView = DepthSurfaceView(context, *state.depth);
timer.phase(PhaseReadTarget);
if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone);
}
timer.phase(PhasePrepare);
// The template's proof (rules R6/R7): ProveCurrent, T1 included, the alias checks the trimmed
// key leaves to a hit repeated; a failure removes the template from the cache (the batch keeps
@ -2532,7 +2506,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
auto pipeline = recipe.pipeline.lock();
if (pipeline == nullptr) return miss(DrawRecipeMiss::ObjectsGone);
auto framebuffer = recipe.framebuffer.lock();
if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent);
if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent, state.renderLayers);
timer.phase(PhasePipeline);
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
RecordedDraw record;

View File

@ -35,7 +35,10 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) {
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = pipelineLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3};

View File

@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
@ -161,6 +162,7 @@ struct HostImports {
std::map<VkDevice, std::vector<std::shared_ptr<RetiredImport>>> parked;
std::uint64_t liveBytes = 0;
std::map<std::uint64_t, ImportFailure> failed;
std::map<std::uint64_t, ImportFailure> failedJoins;
std::set<std::uint64_t> unaliasedSpans;
// Registry generation the imports were last reconciled with.
std::uint64_t refreshedGeneration = 0;
@ -170,6 +172,8 @@ struct HostImports {
VkDevice watchDevice = VK_NULL_HANDLE;
bool unwatchImports = false;
bool unwatchDmaBufImports = false;
std::uint64_t useCounter = 0;
std::uint64_t buildFloor = 0;
};
HostImports& Imports() {
@ -210,12 +214,13 @@ struct RetiredImport {
const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
void retireImport(VkDevice device, HostImports& state, std::map<std::uint64_t, HostImport>::iterator it, const GuestAllocations::Lease& lease, bool hold = true) {
// Results shadowed for the import reach its (old) buffer first where the memory is still a
// readable registered range (the import retires because its registration vanished or changed
// size); the holder below outlives the batch that copies them.
RetireShadow(device, it->second, [&lease](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end); });
RetireShadow(device, it->second, [&lease, joined = it->second.joined](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end) || (joined && joinedCovers(lease, begin, end)); });
++state.epoch;
state.liveBytes -= it->second.bytes;
auto holder = std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it));
@ -579,6 +584,13 @@ std::shared_ptr<const GuestAllocations::Range> leasedRangeOwner(const GuestAlloc
return found != lease.end() && (*found)->address == base ? *found : nullptr;
}
std::shared_ptr<const GuestAllocations::Range> leasedRangeOwner(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; });
if (found == lease.begin()) return nullptr;
const auto& range = *std::prev(found);
return begin >= range->address && end <= range->address + range->bytes ? range : nullptr;
}
struct ImportRun {
std::uint64_t base = 0;
std::uint64_t bytes = 0;
@ -630,6 +642,34 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u
return !run.ranges.empty() && end <= run.base + run.bytes;
}
ImportRun crossedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
auto last = leasedIndexContaining(lease, begin);
if (last == lease.size()) return {};
const auto first = last;
while (lease[last]->address + lease[last]->bytes < end) {
if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {};
++last;
}
if (first == last) return {};
ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}};
run.ranges.assign(lease.begin() + static_cast<std::ptrdiff_t>(first), lease.begin() + static_cast<std::ptrdiff_t>(last) + 1);
return run;
}
ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto window = importWindow.load(std::memory_order_relaxed);
if (window == 0) return {};
auto run = crossedRun(lease, begin, end);
if (run.ranges.empty()) return run;
if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run;
if (begin == run.base && end == run.base + run.bytes) return run;
return {};
}
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
return !crossedRun(lease, begin, end).ranges.empty();
}
template<typename Entry>
bool sameRun(const Entry& entry, const ImportRun& run) {
if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false;
@ -652,45 +692,173 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) {
}
bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) {
return sameRun(entry, importRun(lease, entry.base));
if (entry.ranges.size() == 1 && !entry.joined && entry.bytes < entry.ranges.front()->bytes) {
const auto current = leasedRangeOwner(lease, entry.base, entry.base + entry.bytes);
return current != nullptr && !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front());
}
return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base));
}
bool pointerHeld(const HostImports& state, const HostImport& entry) {
return entry.lastUse >= state.buildFloor;
}
bool evictable(const HostImports& state, const HostImport& entry) {
constexpr std::uint64_t protectedUses = 256;
return !pointerHeld(state, entry) && entry.lastUse + protectedUses < state.useCounter;
}
std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) {
std::uint64_t freed = 0;
while (freed < wanted) {
auto oldest = state.imports.end();
for (auto it = state.imports.begin(); it != state.imports.end(); ++it) {
if (evictable(state, it->second) && (oldest == state.imports.end() || it->second.lastUse < oldest->second.lastUse)) oldest = it;
}
if (oldest == state.imports.end()) break;
freed += oldest->second.bytes;
retireImport(context.device, state, oldest, lease);
}
return freed;
}
std::uint64_t physicalMemoryBytes() {
#ifdef _WIN32
MEMORYSTATUSEX status{};
status.dwLength = sizeof(status);
return GlobalMemoryStatusEx(&status) ? status.ullTotalPhys : 0;
#else
const auto pages = sysconf(_SC_PHYS_PAGES);
const auto pageSize = sysconf(_SC_PAGESIZE);
return pages > 0 && pageSize > 0 ? static_cast<std::uint64_t>(pages) * static_cast<std::uint64_t>(pageSize) : 0;
#endif
}
std::atomic<std::uint64_t> budgetOverride{0};
// Pinned imports count against the driver's system memory heap, which imports and type-3 mirrors share; past it
// ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else the larger of 6 GiB and half the physical
// memory, at most the largest host heap less 2 GiB.
std::uint64_t importBudget(const Context& context) {
if (const auto set = budgetOverride.load(std::memory_order_relaxed); set != 0) return set;
static const std::uint64_t configured = []() -> std::uint64_t {
const char* value = std::getenv("APS5_HOST_IMPORT_MIB");
return value != nullptr ? std::strtoull(value, nullptr, 10) << 20u : 0u;
}();
if (configured != 0) return configured;
std::uint64_t hostHeap = 0;
for (std::uint32_t index = 0; index < context.memory.memoryHeapCount; ++index) {
if ((context.memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max<std::uint64_t>(hostHeap, context.memory.memoryHeaps[index].size);
}
const auto fromMemory = std::max<std::uint64_t>(6144ull << 20u, physicalMemoryBytes() / 2);
constexpr std::uint64_t reserve = 2048ull << 20u;
return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory;
}
std::atomic<std::uint64_t> rangeImportWindow{64ull << 20u};
// The import of an access to a registered range larger than the range window: the cells the access touches, grown to
// the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of
// replacing each other's.
ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) {
const auto window = rangeImportWindow.load(std::memory_order_relaxed);
if (run.ranges.size() != 1 || window == 0 || alignment == 0 || window % alignment != 0) return run;
const auto& range = *run.ranges.front();
if (range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return run;
const auto lastCell = range.bytes / window - 1;
const auto cell = [&](std::uint64_t address) { return std::min((address - range.address) / window, lastCell); };
auto windowBegin = range.address + cell(begin) * window;
const auto endCell = cell(end - 1);
auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window;
for (bool grown = true; grown;) {
grown = false;
auto it = state.imports.upper_bound(windowBegin);
if (it != state.imports.begin()) --it;
for (; it != state.imports.end() && it->first < windowEnd; ++it) {
const auto importEnd = it->second.base + it->second.bytes;
if (importEnd <= windowBegin || it->second.base < range.address || importEnd > range.address + range.bytes) continue;
if (it->second.base < windowBegin || importEnd > windowEnd) grown = true;
windowBegin = std::min(windowBegin, it->second.base);
windowEnd = std::max(windowEnd, importEnd);
}
}
if (windowBegin % alignment != 0 || (windowEnd - windowBegin) % alignment != 0) return run;
run.base = windowBegin;
run.bytes = windowEnd - windowBegin;
return run;
}
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end);
bool importsStale(const Context& context, const HostImports& state);
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict);
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease) {
if (!importsStale(context, state)) {
if (const auto* current = findImport(state, address, address + 1)) return current;
// The bytes retireLeastRecent could free outside [begin, end).
std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end) {
std::uint64_t bytes = 0;
for (const auto& [base, entry] : state.imports) {
if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.bytes;
}
return bytes;
}
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) {
const bool fresh = !importsStale(context, state);
if (fresh && end != 0) {
if (const auto* current = findImport(state, address, end)) return current;
} else if (!fresh) {
if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current;
}
auto run = importRun(lease, address);
// Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the
// first byte is shorter than that, so only an import of the whole run is a hit; a window is replaced below.
if (fresh && end == 0) {
if (const auto* current = findImport(state, address, run.ranges.empty() ? address + 1 : run.base + run.bytes)) return current;
}
if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment);
if (run.ranges.empty()) return nullptr;
return importRunAt(context, state, std::move(run), false, address, lease, end, evict);
}
const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base;
if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes;
auto run = joinedRun(lease, begin, end);
if (run.ranges.empty()) return nullptr;
if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr;
return importRunAt(context, state, std::move(run), true, begin, lease, end, true);
}
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict) {
const auto base = run.base;
const auto bytes = run.bytes;
if (const auto found = state.imports.find(base); found != state.imports.end()) {
if (sameRun(found->second, run)) return &found->second;
retireImport(context.device, state, found, lease);
}
for (auto it = state.imports.lower_bound(base); it != state.imports.end() && it->first < base + bytes;) {
const auto next = std::next(it);
retireImport(context.device, state, it, lease);
it = next;
}
if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease);
auto& failures = joined ? state.failedJoins : state.failed;
const auto alignment = context.hostImportAlignment;
if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr;
if (const auto failed = state.failed.find(base); failed != state.failed.end()) {
if (sameRun(failed->second, run)) return nullptr;
state.failed.erase(failed);
if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())});
return nullptr;
}
// Pinned imports count against the driver's system memory budget; past it ordinary host
// allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the
// registered memory of address-based shaders; past it they copy gigabytes per dispatch).
static const std::uint64_t budget = [] {
const char* value = std::getenv("APS5_HOST_IMPORT_MIB");
return (value ? std::strtoull(value, nullptr, 10) : 6144ull) << 20u;
}();
const auto live = state.liveBytes;
if (live + bytes > budget) {
if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second;
if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr;
if (const auto failed = failures.find(base); failed != failures.end()) {
if (sameRun(failed->second, run)) return nullptr;
failures.erase(failed);
}
// The imports this one replaces overlap it; none may be held by a build, and the import must fit the budget (with
// what may be evicted) before any of them is retired.
auto first = state.imports.lower_bound(base);
if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first;
std::uint64_t replaced = 0;
// A join may replace imports this build already looked up: it runs before any region confirms an import
// (UploadFinish's pre-pass re-resolves every pointer after it), and a retired import stays alive through the
// hold (KeepThroughHold). Refusing it there would keep the run copied for good.
const auto held = [&](const HostImport& entry) { return !joined && pointerHeld(state, entry); };
for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) {
if (held(it->second)) return nullptr;
replaced += it->second.bytes;
}
const auto budget = importBudget(context);
const auto live = state.liveBytes - replaced;
if (live + bytes > budget && (!evict || live + bytes - evictableBytes(state, base, base + bytes) > budget)) {
// A refused import turns every later use of the range into CPU copies, so say so.
static std::uint64_t refused = 0, refusedBytes = 0;
static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60);
@ -698,10 +866,16 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
refusedBytes += bytes;
if (std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) {
lastReport = std::chrono::steady_clock::now();
std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast<unsigned long long>(live >> 20u), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(refused), static_cast<unsigned long long>(refusedBytes >> 20u), static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes));
std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast<unsigned long long>(state.liveBytes >> 20u), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(refused), static_cast<unsigned long long>(refusedBytes >> 20u), static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes));
}
return nullptr;
}
for (auto it = first; it != state.imports.end() && it->first < base + bytes;) {
const auto next = std::next(it);
retireImport(context.device, state, it, lease);
it = next;
}
if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget);
HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
entry.range = run.ranges.front();
entry.ranges = std::move(run.ranges);
@ -713,7 +887,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
for (std::uint64_t cursor = base; cursor < base + bytes;) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<const void*>(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
const auto protection = info.Protect & 0xffu;
@ -725,7 +899,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
cursor = reinterpret_cast<std::uint64_t>(info.BaseAddress) + info.RegionSize;
}
if (!writable && readOnly) {
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
if (!writable) {
@ -738,13 +912,17 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), static_cast<std::size_t>(bytes));
} catch (const std::runtime_error&) {
if (entry.ranges.size() == 1) throw;
if (joined) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
state.unaliasedSpans.insert(base);
return importAllocation(context, state, address, lease);
return importAllocation(context, state, address, lease, end, evict);
}
}
#else
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(base), static_cast<std::size_t>(bytes), true)) {
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
#endif
@ -775,7 +953,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result));
#ifdef _WIN32
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
@ -805,16 +983,12 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
importedBytes += bytes;
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
state.liveBytes += bytes;
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0);
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : "");
entry.joined = joined;
entry.lastUse = ++state.useCounter;
return &state.imports.emplace(base, entry).first->second;
}
// The lease is in address order (it is built from the registry map), so a range is found by binary search.
const GuestAllocations::Range* leasedRangeAt(const GuestAllocations::Lease& lease, std::uint64_t base) {
const auto found = std::lower_bound(lease.begin(), lease.end(), base, [](const auto& range, std::uint64_t value) { return range->address < value; });
return found != lease.end() && (*found)->address == base ? found->get() : nullptr;
}
// The readable registered range containing [begin, end), or null.
const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; });
@ -839,6 +1013,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
}
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
state.device = context.device;
state.destroyBuffer = context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer");
state.freeMemory = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
@ -851,19 +1026,19 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
if (generation == state.refreshedGeneration) return;
state.refreshedGeneration = generation;
for (auto it = state.imports.begin(); it != state.imports.end();) {
const auto* range = leasedRangeAt(lease, it->first);
if (range != nullptr && sameRange(it->second, lease)) {
if (sameRange(it->second, lease)) {
if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes);
++it;
continue;
}
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
if (trace && range != nullptr && range->bytes == it->second.bytes) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast<unsigned long long>(it->first), static_cast<unsigned long long>(it->second.bytes));
if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast<unsigned long long>(it->first), static_cast<unsigned long long>(it->second.bytes));
const auto next = std::next(it);
retireImport(context.device, state, it, lease);
it = next;
}
for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it);
for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it);
}
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) {
@ -871,7 +1046,15 @@ const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint6
if (found == state.imports.begin()) return nullptr;
--found;
const auto& entry = found->second;
return begin >= entry.base && end <= entry.base + entry.bytes ? &entry : nullptr;
if (begin < entry.base || end > entry.base + entry.bytes) return nullptr;
entry.lastUse = ++state.useCounter;
return &entry;
}
const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
if (!runCovers(lease, begin, end)) return findImport(state, begin, end);
const auto* entry = importAllocation(context, state, begin, lease, end);
return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr;
}
// Whether the imports need reconciling before a lookup: the registry changed since the last walk.
@ -1784,6 +1967,10 @@ MirrorStats MirrorCounters() {
}
void ClearImageMirrors(VkDevice device) {
auto space = Spaces().current.load();
if (space != nullptr && space->device == device) {
Spaces().current.compare_exchange_strong(space, std::shared_ptr<const GuestBufferMemory::AddressSpace>{});
}
auto& state = Mirrors();
std::map<std::uint64_t, std::shared_ptr<ImageMirror>> entries;
{
@ -1794,7 +1981,6 @@ void ClearImageMirrors(VkDevice device) {
state.heapBytes = 0;
state.device = VK_NULL_HANDLE;
}
Spaces().current.store(nullptr);
}
void ClearHostImports(VkDevice device) {
@ -1812,6 +1998,7 @@ void ClearHostImports(VkDevice device) {
state.imports.clear();
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
state.device = VK_NULL_HANDLE;
state.refreshedGeneration = 0;
++state.epoch;
@ -2000,6 +2187,20 @@ void SetHostImportWindow(std::uint64_t bytes) {
importWindow.store(bytes, std::memory_order_relaxed);
}
void SetHostImportRangeWindow(std::uint64_t bytes) {
rangeImportWindow.store(bytes, std::memory_order_relaxed);
}
void SetHostImportBudget(std::uint64_t bytes) {
budgetOverride.store(bytes, std::memory_order_relaxed);
}
std::uint64_t HostImportLiveBytes() {
auto& state = Imports();
std::lock_guard lock(state.mutex);
return state.liveBytes;
}
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) {
if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return nullptr;
ensurePinWaiter();
@ -2007,13 +2208,14 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s
std::lock_guard lock(state.mutex);
// A hit is only valid while the registry has not changed since the imports were reconciled.
if (!importsStale(context, state)) {
if (const auto* entry = findImport(state, address, address + bytes)) return entry;
if (const auto* entry = findImport(state, address, address + bytes)) {
entry->lastUse = ++state.useCounter;
return entry;
}
}
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
refreshImports(context, state, lease);
if (!runCovers(lease, address, address + bytes)) return nullptr;
const auto* entry = importAllocation(context, state, address, lease);
return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr;
return importAccess(context, state, address, address + bytes, lease);
}
bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) {
@ -2036,7 +2238,7 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped&
if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue;
try {
if (!GuestMemory::GpuMutex().HeldByThisThread() && wouldRetireImport(state, importRun(current, range->address))) return false;
if (importAllocation(context, state, range->address, current) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base);
if (importAllocation(context, state, range->address, current, 0, false) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base);
} catch (const std::exception& error) {
std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast<unsigned long long>(range->address), range->bytes, error.what());
}
@ -2136,6 +2338,13 @@ void GuestBufferMemory::AcquireRegistered() {
else if (current->generation != generation) spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed);
else if (current->importsEpoch != state.epoch) spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed);
else hit = true;
importFloor = state.useCounter + 1;
if (hit) {
const auto stamp = ++state.useCounter;
for (const auto& region : current->base) {
if (region.direct != nullptr) region.direct->lastUse = stamp;
}
}
}
if (hit) {
for (auto* mirror : current->sparseMirrors) {
@ -2174,6 +2383,8 @@ void GuestBufferMemory::AcquireRegistered() {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (importable) refreshImports(context, state, lease);
if (importFloor == 0) importFloor = state.useCounter + 1;
state.buildFloor = importFloor;
// Taken before the pass: the imports it takes are the registry's ones (just reconciled, so
// none is retired by a make below), and a mirror's preparation may sync the recorder, whose
// completions can retire an import already taken; that moves the epoch, and Upload then
@ -2195,7 +2406,7 @@ void GuestBufferMemory::AcquireRegistered() {
if (importable) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
entry = importAllocation(context, state, range->address, lease);
entry = importAllocation(context, state, range->address, lease, 0, false);
}
lap(timing.importsUs, at);
if (entry != nullptr) {
@ -2257,6 +2468,20 @@ void GuestBufferMemory::AcquireRegistered() {
publish = false;
spaces.unpublished.fetch_add(1, std::memory_order_relaxed);
} else {
std::size_t kept = 0;
for (std::size_t index = 0; index < regions.size(); ++index) {
auto& region = regions[index];
if (kept != 0) {
auto& previous = regions[kept - 1];
if (region.direct != nullptr && previous.direct == region.direct && previous.end == region.begin && previous.writable == region.writable && region.mirror == nullptr && previous.mirror == nullptr && !region.sparse && !previous.sparse && region.end <= region.direct->base + region.direct->bytes) {
previous.end = region.end;
continue;
}
}
if (kept != index) regions[kept] = std::move(region);
++kept;
}
regions.erase(regions.begin() + static_cast<std::ptrdiff_t>(kept), regions.end());
for (const auto& region : regions) {
if (region.direct != nullptr || region.mirror != nullptr) ranges.push_back(addressRange(region));
}
@ -2830,6 +3055,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) {
if (context.hostImportAlignment != 0) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (importFloor == 0) importFloor = state.useCounter + 1;
const bool stale = importsStale(context, state);
for (auto& region : regions) {
if (region.mirror != nullptr) continue;
@ -2904,10 +3130,19 @@ void GuestBufferMemory::UploadPrepare(bool addressable) {
void GuestBufferMemory::UploadFinish(bool addressable) {
Require(prepared && !uploaded, "guest memory upload was not prepared");
uploaded = true;
for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "written buffer range");
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto started = std::chrono::steady_clock::now();
for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
if (space != nullptr) {
for (const auto& region : space->base) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
}
uploaded = true;
for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "written buffer range");
if (context.hostImportAlignment != 0) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
state.buildFloor = importFloor != 0 ? importFloor : state.useCounter + 1;
}
// Registered ranges to import from. Address-based shaders hold their lease until write-back;
// descriptor-only uploads acquire one only when the registry changed or an import must be made
// (a lease copies every registered range's shared_ptr under the registry lock), so the guest can
@ -2937,6 +3172,17 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
++Mirrors().subranges;
}
bool importsRefreshed = false;
if (context.hostImportAlignment != 0) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (importsStale(context, state)) refreshImports(context, state, importRanges());
importsRefreshed = true;
for (const auto& region : regions) {
if (!region.pending || region.mirror != nullptr || region.direct != nullptr || sparseHeapCandidate(region)) continue;
if (findImport(state, region.begin, region.end) != nullptr || runCovers(importRanges(), region.begin, region.end)) continue;
static_cast<void>(importJoined(context, state, region.begin, region.end, importRanges()));
}
}
if (space != nullptr && context.hostImportAlignment != 0) {
// The space's regions were confirmed against the imports when it was built; one check that
// none was retired since (a mirror refresh or a flush between the stages can reconcile
@ -2994,7 +3240,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
region.direct = nullptr;
if (entry == nullptr) entry = findImport(state, region.begin, region.end);
if (entry == nullptr) {
if (runCovers(importRanges(), region.begin, region.end)) entry = importAllocation(context, state, region.begin, importRanges());
entry = importAccess(context, state, region.begin, region.end, importRanges());
}
// A staged region (see stagingEligible) is copied out of the import even when aligned;
// without a recorder to record the copies it binds in place like any other.

View File

@ -5,6 +5,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include <algorithm>
#include <array>
#include <atomic>
@ -153,6 +154,7 @@ public:
}
Context context;
std::shared_ptr<std::atomic<bool>> active = std::make_shared<std::atomic<bool>>(true);
ColorTarget target;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
@ -175,10 +177,19 @@ struct GuardedRange {
std::uint64_t address;
std::uint64_t bytes;
std::uint32_t samples;
std::shared_ptr<std::atomic<bool>> active;
};
class PrimedSharedMutex : public std::shared_mutex {
public:
PrimedSharedMutex() {
lock();
unlock();
}
};
std::shared_mutex& guardedMutex() {
static std::shared_mutex mutex;
static PrimedSharedMutex mutex;
return mutex;
}
@ -196,7 +207,17 @@ void guardReads(std::uint64_t address, std::size_t bytes) {
RequireNoMultisampleTarget(address, bytes, "a CPU read");
}
void guard(const Context& context, const ColorTarget& target) {
void retireUnmapped(std::uintptr_t address, std::size_t bytes) {
std::unique_lock lock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) {
if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false;
range.active->store(false, std::memory_order_release);
return true;
});
anyGuarded().store(!guarded().empty(), std::memory_order_release);
}
void guard(const Context& context, const ColorTarget& target, const std::shared_ptr<std::atomic<bool>>& active) {
auto bytes = std::max<std::uint64_t>(target.bytes * target.samples, static_cast<std::uint64_t>(target.extent.width) * target.extent.height * target.elementBytes * target.samples);
if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) {
const auto log2Samples = std::countr_zero(target.samples);
@ -208,10 +229,13 @@ void guard(const Context& context, const ColorTarget& target) {
bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples;
}
std::unique_lock lock(guardedMutex());
guarded().push_back({context.device, target.address, bytes, target.samples});
guarded().push_back({context.device, target.address, bytes, target.samples, active});
anyGuarded().store(true, std::memory_order_release);
static std::once_flag installed;
std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); });
std::call_once(installed, [] {
GuestMemory::SetReadGuard(&guardReads);
});
GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&retireUnmapped);
}
std::mutex& targetsMutex() {
@ -226,14 +250,15 @@ std::vector<std::unique_ptr<MultisampleTarget>>& targets() {
MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
for (const auto& target : targets()) {
if (!target->active->load(std::memory_order_acquire)) continue;
if (target->context.device != context.device || !sameTarget(target->target, use)) continue;
target->cmask.CheckAddress(use.cmaskAddress);
target->ApplyFastClear(use);
return *target;
}
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
guard(context, use);
auto& created = *targets().back();
guard(context, use, created.active);
if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
created.ApplyFastClear(use);
return created;
@ -380,6 +405,7 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source,
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) {
std::lock_guard lock(targetsMutex());
for (const auto& target : targets()) {
if (!target->active->load(std::memory_order_acquire)) continue;
target->colorFill.NoteFill(address, bytes, pattern);
const auto before = target->cmask.Keys();
target->cmask.NoteFill(address, bytes, pattern);
@ -395,6 +421,7 @@ void ClearMultisampleTargets(VkDevice device) {
std::unique_lock guardLock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) { return range.device == device; });
anyGuarded().store(!guarded().empty(), std::memory_order_release);
if (guarded().empty()) GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr);
}
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) {

View File

@ -69,10 +69,11 @@ bool HasStencil(VkFormat format) {
return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_S8_UINT;
}
Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent) : context(context) {
Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent, std::uint32_t layers) : context(context) {
// Cached objects outlive their device's teardown; they must not keep its buffer pool alive past it.
this->context.bufferPool.reset();
Require(extent.width != 0 && extent.height != 0 && extent.width <= context.limits.maxFramebufferWidth && extent.height <= context.limits.maxFramebufferHeight, "framebuffer extent exceeds device limits");
Require(layers != 0 && layers <= context.limits.maxFramebufferLayers, "framebuffer layers exceed device limits");
views.assign(targets.begin(), targets.end());
if (renderPass == VK_NULL_HANDLE) return;
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
@ -81,7 +82,7 @@ Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::s
framebufferInfo.pAttachments = targets.empty() ? nullptr : targets.data();
framebufferInfo.width = extent.width;
framebufferInfo.height = extent.height;
framebufferInfo.layers = 1;
framebufferInfo.layers = layers;
Check(context.Function<PFN_vkCreateFramebuffer>("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer");
}
@ -498,13 +499,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
return;
}
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules);
else Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
else {
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
}
if (alternateAlpha) {
blend.pAttachments = secondChannels.data();
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines");
}
if (state.depthBiasPerFace) {
raster.cullMode = VK_CULL_MODE_FRONT_BIT;
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines");
}
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
@ -571,7 +577,7 @@ VkPipelineLayout Pipeline::Layout() const {
return layout;
}
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent) {
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers, std::span<const std::uint8_t> persistent) {
Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments");
bool resident = true;
for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0));
@ -586,7 +592,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
return false;
});
for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) {
if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
if (it->extent.width != extent.width || it->extent.height != extent.height || it->layers != layers || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
// View handles are recycled once a StorageTexture is destroyed, so the owners must be the
// very objects the views were made for.
bool same = true;
@ -596,7 +602,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
return framebuffers.back().framebuffer;
}
}
auto framebuffer = std::make_shared<Framebuffer>(context, renderPass, targets, extent);
auto framebuffer = std::make_shared<Framebuffer>(context, renderPass, targets, extent, layers);
if (!resident) return framebuffer;
// Beyond the bound the least recently used unreferenced entry goes.
constexpr std::size_t bound = 8;
@ -610,6 +616,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
entry.owners.assign(owners.begin(), owners.end());
for (const auto& owner : owners) entry.owned.push_back(owner != nullptr);
entry.extent = extent;
entry.layers = layers;
entry.framebuffer = framebuffer;
framebuffers.push_back(std::move(entry));
return framebuffer;
@ -636,7 +643,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V
}
VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR};
rendering.renderArea = {{0, 0}, extent};
rendering.layerCount = 1;
rendering.layerCount = state.renderLayers;
rendering.colorAttachmentCount = static_cast<std::uint32_t>(colors.size());
rendering.pColorAttachments = colors.empty() ? nullptr : colors.data();
if (depthFormat != VK_FORMAT_UNDEFINED) rendering.pDepthAttachment = &depth;
@ -825,6 +832,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, tessellation.domain);
append(key, tessellation.partitioning);
append(key, tessellation.outputTopology);
append(key, tessellation.patchesPerGroup);
}
}

View File

@ -106,11 +106,15 @@ void PipelineCache::load(std::vector<std::byte>& initialData) {
void PipelineCache::save(bool final) {
const auto getData = context.Function<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
std::size_t size = 0;
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
std::vector<std::byte> file(sizeof(FileHeader) + size);
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
file.resize(sizeof(FileHeader) + size);
std::vector<std::byte> file;
{
auto synchronizationLock = synchronization.LockForSnapshot();
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
file.resize(sizeof(FileHeader) + size);
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
file.resize(sizeof(FileHeader) + size);
}
const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))};
std::memcpy(file.data(), &header, sizeof(header));
const auto started = std::chrono::steady_clock::now();

View File

@ -205,7 +205,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
}
VkPipeline handle = VK_NULL_HANDLE;
if (partsOnly) lock.unlock();
const auto created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
}
if (partsOnly) lock.lock();
Check(created, "vkCreateGraphicsPipelines library");
if (const auto raced = cache.find(key); raced != cache.end()) {
@ -234,7 +238,10 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked};
create.layout = info.layout;
VkPipeline pipeline = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
}
++device.counters.linked;
if (optimize && optimized != nullptr) {
auto slot = std::make_shared<OptimizedPipeline>();
@ -243,8 +250,9 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
*optimized = slot;
const auto createPipelines = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines");
const auto cache = context.pipelineCache;
const auto cacheSynchronization = context.pipelineCacheSynchronization;
const auto libraryLayout = fullLayout;
enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] {
enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] {
if (slot->released.load()) return;
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());
@ -253,7 +261,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT;
create.layout = libraryLayout;
VkPipeline result = VK_NULL_HANDLE;
if (createPipelines(slot->device, cache, 1, &create, nullptr, &result) != VK_SUCCESS) return;
const auto pipelineCreationResult = [&] {
auto pipelineCacheLock = cacheSynchronization != nullptr ? cacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
return createPipelines(slot->device, cache, 1, &create, nullptr, &result);
}();
if (pipelineCreationResult != VK_SUCCESS) return;
slot->handle.store(result);
if (slot->released.load()) {
if (const auto handle = slot->handle.exchange(VK_NULL_HANDLE)) slot->destroy(slot->device, handle, nullptr);

View File

@ -1998,7 +1998,10 @@ bool Recorder::gpuSampleCounter() {
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = sampleLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes};
@ -2009,7 +2012,10 @@ bool Recorder::gpuSampleCounter() {
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps");
pipelineInfo.stage.module = module;
pipelineInfo.layout = dumpLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
} catch (const std::exception& error) {
@ -2286,7 +2292,10 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = meshArgumentLayout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
meshArgumentState = 1;
} catch (const std::exception& error) {
@ -3309,9 +3318,6 @@ void Recorder::SyncThrough(std::uint64_t address, std::size_t bytes, bool waitUn
// later batch's own ordering against this write-back is its recorder's business at record
// time (FillBuffer and DispatchIndirect consult copiedWriters; a later copied writer's
// write-back runs after this one in finish order; a label behind completions lands after).
// Known gap, not introduced here: a later batch writing the same range GPU-direct in place
// (a writable region bound in place, or a GPU copy-back) is not ordered against this
// write-back by the resource build; it went from deterministically stale to racy.
announcedSource = 4;
announcedSite = nullptr;
++holdCounters.completionSyncsSkipped;

View File

@ -113,10 +113,12 @@ namespace AgcDriver::Graphics {
return requiresNonSeamlessCube;
}
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
if (IsSrgbTextureFormat(guestFormat)) return false;
for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) {
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented");
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true;
}
return false;
}
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {

View File

@ -143,6 +143,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
const bool depthExportEnable = (shaderControl & 0x1u) != 0;
const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0;
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0;
const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0;
const auto zOrder = (shaderControl >> 4u) & 0x3u;
const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; };
return ShaderRecompiler::ShaderPixelStageInfo{
@ -164,7 +165,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.pixelKillEnable = pixelKillEnable,
.depthExportEnable = depthExportEnable,
.sampleMaskExportEnable = sampleMaskExportEnable,
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable,
.earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable),
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,

View File

@ -9,6 +9,7 @@
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include <algorithm>
#include <atomic>
#include <memory>
@ -110,6 +111,7 @@ struct CachedTexture {
std::uint64_t sourceVersion = 0;
std::uint64_t accounted = 0;
std::uint64_t present = 0;
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> committed;
};
// Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the
@ -553,7 +555,8 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
// memory first.
const bool depthCopy = depthCompare && guestBytes >= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D;
auto source = StorageTexture::FindPending(address, guestBytes);
if (source != nullptr && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource);
if (source != nullptr && !blockCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
if (source != nullptr && MetadataMoved(*source, resource)) source.reset();
std::optional<DccKeys> keys;
bool clearThroughKeys = false;
@ -625,11 +628,25 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
texture->RefreshDepthCopy();
return texture;
}
if (source != nullptr && blockCopy) {
auto texture = std::make_shared<Texture>(context, source, resource, components, Texture::BlockCopyTag{});
texture->RefreshDepthCopy();
return texture;
}
if (source != nullptr) return std::make_shared<Texture>(context, source, resource, components);
std::vector<std::byte> snapshot(bytes);
ReadTextureSurface(resource, *keys, snapshot);
return std::make_shared<Texture>(context, *context.detiler, resource, components, snapshot, depthCompare);
}
auto snapshotGeneration = generation;
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> committed;
if (source == nullptr && generation == 0) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
if (const auto partial = GuestMemory::CollectCommittedWrites(address, bytes, ranges); partial != 0) {
snapshotGeneration = partial;
committed = std::move(ranges);
}
}
auto& cache = Textures();
const auto key = MakeTextureKey(context.device, words, components, depthCompare);
std::lock_guard lock(cache.mutex);
@ -658,8 +675,13 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
return GuestMemory::EqualsCommitted(address, it->bytes);
};
if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) {
it->generation = generation;
const auto unchanged = [&] {
if (it->committed != committed) return false;
return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation);
};
if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) {
it->generation = snapshotGeneration;
it->committed = std::move(committed);
touchTexture(cache, it);
logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr});
reportTextureCounters();
@ -670,13 +692,17 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
eraseTexture(cache, it);
++cache.counts.stale;
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, generation};
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration};
entry.committed = std::move(committed);
if (source != nullptr) {
entry.source = source;
entry.sourceVersion = source->Version();
if (depthCompare) {
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::DepthCopyTag{});
entry.texture->RefreshDepthCopy();
} else if (blockCopy) {
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::BlockCopyTag{});
entry.texture->RefreshDepthCopy();
} else {
entry.texture = std::make_shared<Texture>(context, source, resource, components);
}
@ -1616,7 +1642,7 @@ void ShaderResources::buildComplete() {
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) {
if (index == std::numeric_limits<std::size_t>::max()) images.push_back(absentImage(binding.layout.binding));
else images.push_back({VK_NULL_HANDLE, textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
else images.push_back({VK_NULL_HANDLE, textureSrgb[index] ? textures[index]->SrgbView(textureFirstLayer[index], textureBaseLevel[index]) : textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
}
Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding");
break;
@ -2288,13 +2314,39 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span
return OwnRefreshFallback::Count;
}
void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || Recorder::InCompletion()) return;
auto* recorder = Recorder::Active();
if (recorder == nullptr) return;
const auto drawWriters = DrawCopiedWriters();
const bool labels = Recorder::PendingCompletionLabels() != 0;
if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return;
if (!recorder->PendingWriteOverlaps(address, bytes)) return;
const auto overlaps = [&](const auto& writers) {
return std::any_of(writers.begin(), writers.end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); });
};
if ((context.copiedWriters != nullptr && overlaps(*context.copiedWriters)) || overlaps(*drawWriters) || (labels && recorder->PendingLabelIn(address, bytes))) {
Recorder::CountSync(4);
recorder->SyncThrough(address, bytes);
}
}
bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofReport* report) {
if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other};
if (!reusable || shaders.empty()) return false;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
for (const auto& range : describedRanges) {
CompletePendingCpuWrites(context, range.address, static_cast<std::size_t>(range.bytes));
if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes));
}
for (const auto& image : storageTextures) {
CompletePendingCpuWrites(context, image->Descriptor().baseAddress, static_cast<std::size_t>(image->GuestBytes()));
if (image->Descriptor().dccAddress != 0) CompletePendingCpuWrites(context, image->Descriptor().dccAddress, DccKeyBytes(image->GuestBytes()));
}
// APS5_NO_FAST_REVALIDATE=1 always repeats the lookups.
static const bool noFast = std::getenv("APS5_NO_FAST_REVALIDATE") != nullptr;
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const auto finish = [&](bool fast, bool ok, ProofFailure failure = ProofFailure::Other) {
if (profile) countRevalidate(fast, ok, start);
if (report != nullptr) report->failure = ok ? ProofFailure::None : failure;
@ -3241,7 +3293,7 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
auto& cache = Textures();
std::lock_guard lock(cache.mutex);
const auto it = findSampledEntry(cache, context.device, record.words, record.components);
if (it == cache.entries.end() || it->texture != record.texture) return nullptr;
if (it == cache.entries.end() || it->texture != record.texture || it->committed.has_value()) return nullptr;
if (it->source == nullptr) it->generation = record.generation;
touchTexture(cache, it);
logLookup({record.texture.get(), record.resource, record.guestBytes, keys, record.source != nullptr ? 0 : record.generation, record.source.get()});
@ -3281,12 +3333,14 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
continue;
}
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
RequireDegammaFormat(resource.format, binding.imageSamplers[element], shaderSamplers);
const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers);
RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers);
const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray;
if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
const VkComponentMapping components = ViewComponents(resource);
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
std::shared_ptr<Texture> texture;
const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element);
if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture;
@ -3306,6 +3360,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
}
RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers);
RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers);
if (forcedSrgb && !texture->HasSrgbView()) Require(false, "a texture of guest format " + std::to_string(resource.format) + ", which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding, which is not implemented");
if (!shaderSamplers.empty()) {
const bool singleLevel = DescriptorSingleLevel(words);
const auto firstSampler = static_cast<std::size_t>(shaderSamplers.data() - samplers.data());
@ -3323,6 +3378,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
textures.push_back(std::move(texture));
textureFirstLayer.push_back(firstLayer);
textureBaseLevel.push_back(baseLevel);
textureSrgb.push_back(forcedSrgb);
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
item.imageAllocations.push_back(textures.size() - 1);
}
@ -3351,6 +3407,8 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
const auto mip = std::min(resource.baseLevel + mipOffset, resource.mipCount - 1u);
Require(resource.minLod <= mip * 256u, "guest storage texture descriptor clamps its minimum LOD above the level it addresses, which is not implemented");
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
// The same surface as the previous element: its image was just looked up and refreshed.
if (sameAsPrevious && StorageDedupeEnabled()) storageTextures.push_back(storageTextures.back());
else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes));
@ -3515,7 +3573,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() {
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved) const {
if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {};
const auto reads = guestMemory.InPlaceReads();
const auto maxSnapshotBytes = MaxDrawSnapshotBytes();
const auto maxSnapshotBytes = maxDrawSnapshotBytes;
std::size_t totalSnapshotBytes = 0;
auto result = std::make_shared<DrawBindings>();
std::vector<std::size_t> selected;

View File

@ -49,6 +49,7 @@ struct Module {
std::uint32_t cullDistances = 0;
bool primitiveIndices = false;
bool fragmentBarycentric = false;
bool layerOutput = false;
std::map<std::uint32_t, std::vector<std::uint32_t>> modes;
const std::vector<std::uint32_t>& Type(std::uint32_t id) const {
@ -134,13 +135,14 @@ struct Module {
return;
}
if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = true;
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool"), "unsupported mesh built-in");
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in");
return;
}
const auto signature = Signature(type);
if (vertex && storage == spv::StorageClassInput) {
Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input");
} else if (vertex && storage == spv::StorageClassOutput) {
if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return;
Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output");
position = true;
} else {
@ -149,7 +151,7 @@ struct Module {
}
};
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) {
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) {
using Stage = ShaderRecompiler::ShaderStage;
Require(compiled.program != nullptr, "missing compiled shader");
const auto& shader = *compiled.program;
@ -199,6 +201,11 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(fragmentShaderBarycentric, "device does not support enabled VK_KHR_fragment_shader_barycentric with fragmentShaderBarycentric");
module.fragmentBarycentric = true;
}
const bool isLayerCapability = capability == spv::CapabilityShaderViewportIndexLayerEXT;
if (isLayerCapability) {
Require(viewportIndexLayer, "device does not support enabled VK_EXT_shader_viewport_index_layer");
module.layerOutput = true;
}
VkSubgroupFeatureFlags subgroupOperations = 0;
switch (capability) {
case spv::CapabilityGroupNonUniform: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; break;
@ -295,7 +302,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
isFeatureCapability ||
isDescriptorIndexingCapability ||
((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) ||
(imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT),
(imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT) ||
isLayerCapability,
std::string("SPIR-V requires unsupported device capability ") +
std::to_string(static_cast<std::uint32_t>(capability)));
@ -320,7 +328,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
break;
}
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
break;
}
case spv::OpDecorateId:
@ -433,9 +441,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints});
Require(module.modes.size() == 1, "unsupported tessellation-control execution mode");
} else if (evaluation) {
mode(state.rectList ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {});
mode(state.rectList ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {});
mode(spv::ExecutionModeVertexOrderCw, {});
Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing");
const bool quads = state.rectList || state.stages.tessellation->domain == 2u;
const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u;
mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {});
mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {});
mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {});
Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode");
} else if (geometry) {
mode(spv::ExecutionModeTriangles, {});
@ -506,7 +517,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
}
} else if (variable.storage == spv::StorageClassPushConstant) {
Require(!push && (!shader.pushConstants.empty() || mesh) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
Require(!push && (!shader.pushConstants.empty() || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
push = true;
Require(type.size() == 3 && module.decorations[typeId].block, "push constant variable must be a Block struct with exactly one member");
const auto offset = module.offsets.find({typeId, 0});
@ -574,7 +585,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage");
}
Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources");
Require(push == (!shader.pushConstants.empty() || mesh), "recompiler push constant metadata disagrees with SPIR-V");
Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V");
for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable");
if (mesh) Require(module.primitiveIndices, "mesh shader does not export primitive indices");
if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position");
@ -583,7 +594,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) {
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) {
using Stage = ShaderRecompiler::ShaderStage;
const bool tessellation = state.stages.path == ShaderPath::Tessellation;
const bool mesh = state.stages.path == ShaderPath::Geometry;
@ -604,7 +615,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
Require(shaders[i].program != nullptr, "missing compiled shader");
Require(shaders[i].stage == expected, "graphics stage order disagrees");
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's");
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance);
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer);
if (i != 0) {
for (const auto& [location, signature] : current.inputs) {
const auto output = previous.outputs.find(location);

View File

@ -2,7 +2,9 @@
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
@ -97,10 +99,30 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt
// The register rules DecodeState and DrawRejection share (the precheck must reject exactly what
// DecodeState would): the masks whose set bits are unsupported, and the depth-control verdict.
// Render target index, viewport index and the misc export vector that carries them are accepted but
// not routed: color targets are single-layer, so layered draws land in layer 0.
constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u);
constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u);
std::uint32_t inactiveVertexPointSize(const QueueState& queue) {
const auto stages = find(queue.context, 0x2d5);
const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig);
const auto raster = find(queue.context, 0x205);
if (stages == queue.context.end() || primitive == queue.userConfig.end() || raster == queue.context.end()) return 0;
if ((stages->second & 0x24u) != 0) return 0;
if ((raster->second & 8u) != 0 && (raster->second & 0x7e0u) != 0x240u) return 0;
switch (primitive->second) {
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 17:
return 1u << 16u;
default:
return 0;
}
}
constexpr std::uint32_t DepthControlMask = ~0x007007f0u;
// EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader
// to run, which it always does here.
@ -355,9 +377,20 @@ std::string singleSampleMsaaRejection(const QueueState& queue) {
return {};
}
constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u;
constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid);
bool coveredCentroidSupported(const QueueState& queue) {
const auto config = find(queue.context, 0x2f8);
if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true;
const auto ena = find(queue.context, 0x1b3);
const auto addr = find(queue.context, 0x1b4);
return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0;
}
std::uint32_t decodeSamples(const Registers& cx) {
const auto config = read(cx, 0x2f8);
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling");
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling");
const auto log2 = config & 7u;
if (log2 == 0) {
Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported");
@ -613,17 +646,22 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing");
const auto config = read(queue.context, 0x2d6);
const auto parameters = read(queue.context, 0x2db);
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u};
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu};
validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts");
validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero");
if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state"));
validate(tess.domain == 1 && tess.partitioning == 2 && tess.outputTopology == 2, "only triangular, fractional-odd, clockwise tessellation is supported by the reference path");
Require(readFloat(queue.context, 0x287) <= 1.0f && readFloat(queue.context, 0x286) >= 63.0f, "tessellation level clamps inside the fractional-odd range [1, 63] are unsupported");
validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer or fractional-odd partitioning and triangle output are supported by the reference path");
result.minTessellationLevel = readFloat(queue.context, 0x287);
result.maxTessellationLevel = readFloat(queue.context, 0x286);
Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum");
result.tessellation = tess;
} else {
validate((value & ~0x0047ec30u) == 0, "unsupported geometry routing, fast launch or wave-ID state");
const bool fastLaunch = ((value >> 19u) & 3u) == 1u;
validate((value & ~(fastLaunch ? 0x004fec30u : 0x0047ec30u)) == 0, "unsupported geometry routing, fast launch or wave-ID state");
const auto group = read(queue.userConfig, 0x25b, RegisterBank::UserConfig);
const auto vertices = (group >> 9u) & 0x1ffu;
const auto primitives = group & 0x1ffu;
validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported");
const auto maxVertices = read(queue.context, 0x1ff);
const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce);
validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly");
@ -634,11 +672,14 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit});
validate(groupPrimitives != 0, "geometry subgroup contains no primitives");
const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader);
validate(vertexWorkgroup || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation");
validate(vertexWorkgroup || fastLaunch || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation");
const auto esgsItemSize = read(queue.context, 0x2ab);
validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE");
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)});
result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize};
const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - 2u;
validate(amplification != 0, "fast launch subgroup amplifies to no primitives");
const auto maxPrimitives = primitives * amplification;
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives});
result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize};
}
return result;
}
@ -693,17 +734,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
}
APS5_LOG_OUT_DEBUG("Topology=%u", static_cast<unsigned>(result.topology));
result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
if ((read(cx, 0x207) & LayerExports) != 0) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207));
}
}
zero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports");
zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports");
Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported");
result.paClVsOutCntl = read(cx, 0x207);
result.samples = decodeSamples(cx);
Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented");
decodeSampleLocations(cx, result);
{
const auto depthControl = read(cx, 0x200);
@ -817,6 +852,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
}
if (result.hasColorTarget) {
result.color = result.colors.front();
result.renderLayers = result.color.layers;
for (const auto& color : result.colors) Require(color.layers == result.renderLayers, "color targets with different layer counts are unsupported");
if (result.renderLayers > 1) {
Require(!result.depth, "layered rendering with a depth target is unsupported");
result.layerExports = read(cx, 0x207) & (1u << 18u);
}
if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)};
} else if (result.depth) {
result.renderExtent = result.depth->extent;
@ -827,6 +868,13 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height);
Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes");
}
if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207));
}
}
const auto xs = readFloat(cx, 0x10f);
const auto xo = readFloat(cx, 0x110);
const auto ys = readFloat(cx, 0x111);
@ -872,11 +920,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
for (std::uint32_t component = 0; component < 4; ++component) {
if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component;
}
state.blendEnable = (blend >> 30u) & 1u;
state.blendEnable = ((blend >> 30u) & 1u) != 0 && (read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0;
if (state.blendEnable && (mapping == 0x1bu || mapping == 0x93u)) throw std::runtime_error("AGC graphics: blending into a color target with a reversed component order is not implemented");
Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported");
if (state.blendEnable) {
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending");
state.srcColorBlendFactor = blendFactor(blend & 0x1fu);
state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu);
state.colorBlendOp = blendOp((blend >> 5u) & 7u);
@ -990,6 +1037,41 @@ std::uint32_t ColorWriteMask(const Registers& context) {
return mask;
}
namespace {
std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest;
throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast<int>(format)));
}
}
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident");
const bool chain = color.mipCount > 1;
GuestTextureResource surface{};
surface.baseAddress = chain ? color.surfaceAddress : color.address;
surface.pipeBankXor = color.pipeBankXor;
surface.width = chain ? color.surfaceExtent.width : color.extent.width;
surface.height = chain ? color.surfaceExtent.height : color.extent.height;
surface.depthOrLastArray = color.depth - 1u;
surface.baseArray = 0;
surface.mipCount = color.mipCount;
surface.baseLevel = 0;
surface.lastLevel = color.mipCount - 1;
surface.tileMode = ColorTextureTileMode(color.tileMode);
surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D;
surface.format = GuestFormatFor(color.format, color.elementBytes);
surface.dstSelX = 4;
surface.dstSelY = 5;
surface.dstSelZ = 6;
surface.dstSelW = 7;
surface.dccAddress = color.dccAddress;
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
surface.dccPipeAligned = color.dccPipeAligned;
return surface;
}
ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto stride = slot * 0xfu;
ColorTarget color{};
@ -1026,16 +1108,16 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported");
if (volume) {
color.depth = (attrib3 & 0x1fffu) + 1u;
Require(maxMip == 0 && (info & 0x10000000u) == 0, "mipmapped or DCC 3D color targets are unsupported");
Require(slice < color.depth, "the color view slice is beyond the 3D surface");
Require(lastSlice == slice, "color views of several 3D slices are unsupported");
Require(maxMip == 0, "mipmapped 3D color targets are unsupported");
Require(slice < color.depth && lastSlice >= slice, "the color view slices are outside the 3D surface");
color.depthSlice = slice;
color.layers = std::min(lastSlice + 1u, color.depth) - slice;
} else if ((attrib3 & 0x1fffu) != 0) {
Require(lastSlice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface");
color.arrayLastSlice = attrib3 & 0x1fffu;
color.arraySlice = slice;
}
if (lastSlice != slice) {
if (!volume && lastSlice != slice) {
const auto exports = find(cx, 0x207);
Require(exports == cx.end() || (exports->second & (1u << 18u)) == 0, "layered draws into a color view of several array slices are unsupported");
}
@ -1063,7 +1145,17 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
}
Require(color.tileMode != ColorTileMode::D64KBX || (color.pipeBankXor & ~0xcf00u) == 0, "unsupported display color pipe/bank XOR column bits");
color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0;
if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1);
if (slice != 0 && !volume) {
color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1);
if (color.arrayLastSlice == 0 && ColorTileModeIsXor(color.tileMode)) {
const auto* equation = FindTextureSwizzleEquation(static_cast<std::uint32_t>(color.tileMode), color.elementBytes);
Require(equation != nullptr, "a sliced color target has no swizzle equation");
const auto coordinates = slice << 24u;
for (std::uint32_t bit = 0; bit < equation->bits.size(); ++bit) {
color.pipeBankXor ^= (std::popcount(equation->bits[bit] & coordinates) & 1u) << bit;
}
}
}
color.address = color.surfaceAddress + mipOffset;
color.bytes = colorLayout.Bytes();
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, colorLayout.Alignment(), true);
@ -1076,7 +1168,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto clear = find(cx, 0x323 + word + stride);
color.clearWords[word] = clear == cx.end() ? 0u : clear->second;
}
if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) {
if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) {
Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported");
Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)");
if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported");
@ -1104,6 +1196,10 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
}
}
}
if (volume && (color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB)) {
color.bytes = DescribeSurface(SurfaceForTarget(color)).guestBytes;
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, colorLayout.Alignment(), true);
}
return color;
}
@ -1120,7 +1216,18 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
if (mode != 2u && mode != 6u && !resolve) return std::nullopt;
ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt};
Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma");
Require(depthPlanesAbsent(cx) || ((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0), "CB metadata pass with depth or stencil work");
const auto value = [&](std::uint32_t offset) {
const auto found = find(cx, offset);
return found == cx.end() ? 0u : found->second;
};
const bool planes = !depthPlanesAbsent(cx);
const auto depthControl = planes ? read(cx, 0x200) : 0u;
const bool depthWork = (depthControl & 0xfu) != 0 && ((depthControl & 8u) != 0 || !depthPassThrough(depthControl));
if (depthWork || (planes && (read(cx, 0x0) & 0xfu) != 0)) {
char text[192];
std::snprintf(text, sizeof(text), "CB metadata pass with depth or stencil work (DB_DEPTH_CONTROL 0x%08x, DB_RENDER_CONTROL 0x%08x, DB_Z_INFO 0x%08x, DB_STENCIL_INFO 0x%08x)", value(0x200), value(0x0), value(0x010), value(0x011));
throw std::runtime_error(text);
}
const auto samples = resolve && rasterizerSamples ? decodeSamples(cx) : 1u;
if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x80, ~0u, "window offset");
@ -1189,7 +1296,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies";
if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported";
}
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported");
if (value(cx, 0x200, word)) {
const auto renderControl = find(cx, 0x000);
@ -1215,7 +1322,8 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) {
if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason;
}
if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason;
if (!coveredCentroidSupported(queue)) return "AGC graphics: COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented";
const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0;
if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason;
if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) {

View File

@ -11,6 +11,7 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp"
#include <algorithm>
#include <array>
#include <bit>
@ -267,7 +268,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
const auto linearBytes = sliceLinearBytes * arrayLayers;
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u;
imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u);
imageInfo.imageType = ImageTypeFor(descriptor.dimension);
imageInfo.format = vkFormat;
imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth};
@ -446,6 +447,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
createFirstLayerView(descriptor, viewInfo);
prepareBaseLevelView(descriptor, viewInfo);
prepareSrgbView(descriptor, viewInfo);
if (profile) {
auto& totals = Profile();
totals.view += timer.lap();
@ -475,6 +477,75 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR
return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount;
}
bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) {
const auto& from = source.Descriptor();
if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false;
if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false;
if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false;
for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) {
const auto width = std::max(descriptor.width >> level, 1u);
const auto height = std::max(descriptor.height >> level, 1u);
if (width % 4u != 0 || height % 4u != 0 || std::max(from.width >> level, 1u) != width / 4u || std::max(from.height >> level, 1u) != height / 4u) return false;
}
const auto geometry = DescribeSurface(descriptor);
return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers;
}
Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) {
try {
Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture");
blockFormat = SampledTextureFormat(context, descriptor.format);
const auto geometry = DescribeSurface(descriptor);
copyWidth = descriptor.width;
copyHeight = descriptor.height;
copyLevels = descriptor.mipCount;
copyLayers = geometry.imageLayers;
copyElementBytes = BytesPerElement(descriptor.format);
APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast<int>(blockFormat));
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = blockFormat;
imageInfo.extent = {descriptor.width, descriptor.height, 1u};
imageInfo.mipLevels = copyLevels;
imageInfo.arrayLayers = copyLayers;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage block copy");
owned = std::make_shared<OwnedImage>(context, image, VK_NULL_HANDLE);
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocationBytes = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory block copy");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy");
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
viewInfo.format = blockFormat;
viewFormat = blockFormat;
viewInfo.components = components;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
ChainMinLod(context, descriptor, viewInfo, minLod);
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView block copy");
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
createFirstLayerView(descriptor, viewInfo);
prepareBaseLevelView(descriptor, viewInfo);
} catch (...) {
release();
throw;
}
}
Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) {
try {
Require(source != nullptr && CanDepthCopyFrom(*source, descriptor), "storage image cannot serve a depth copy of the sampled texture");
@ -537,6 +608,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
bool Texture::RefreshDepthCopy() {
if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false;
if (copiedOnce && storageSource->Version() == copiedVersion) return false;
if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy();
std::uint64_t linearBytes = 0;
for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast<std::uint64_t>(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers;
auto linear = std::make_shared<DeviceBuffer>(context, static_cast<std::size_t>(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
@ -608,6 +680,51 @@ bool Texture::RefreshDepthCopy() {
return true;
}
bool Texture::refreshBlockCopy() {
std::vector<VkImageCopy> regions;
for (std::uint32_t level = 0; level < copyLevels; ++level) {
VkImageCopy region{};
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers};
region.dstSubresource = region.srcSubresource;
region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u};
regions.push_back(region);
}
auto* recorder = Recorder::Active();
std::unique_ptr<CommandBatch> batch;
VkCommandBuffer commands = VK_NULL_HANDLE;
if (recorder != nullptr) {
commands = recorder->Commands();
recorder->Keep(owned);
recorder->Keep(storageSource);
} else {
batch = std::make_unique<CommandBatch>(context);
commands = batch->Handle();
}
const auto barrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT};
VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.image = image;
toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers};
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst);
context.Function<PFN_vkCmdCopyImage>("vkCmdCopyImage")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data());
VkImageMemoryBarrier toShaderRead = toTransferDst;
toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead);
if (batch) batch->SubmitAndWait();
copiedOnce = true;
copiedVersion = storageSource->Version();
return true;
}
Texture::DepthCopyCounts Texture::TakeDepthCopyCounts() {
return {depthCopyTextures.exchange(0, std::memory_order_relaxed), depthCopyRefreshes.exchange(0, std::memory_order_relaxed)};
}
@ -648,6 +765,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
createFirstLayerView(descriptor, viewInfo);
prepareBaseLevelView(descriptor, viewInfo);
prepareSrgbView(descriptor, viewInfo);
if (profile) {
auto& totals = Profile();
totals.view += timer.lap();
@ -700,6 +818,44 @@ void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkIma
baseLevelInfo = viewInfo;
}
void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) {
const auto format = SrgbTwinFormat(context, viewInfo.format);
if (format == VK_FORMAT_UNDEFINED) return;
viewInfo.pNext = nullptr;
viewInfo.format = format;
srgbInfo = viewInfo;
srgbMinLod = EffectiveMinLod(descriptor);
}
VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const {
Require(srgbInfo.has_value(), "AGC graphics: a texture of view format " + std::to_string(viewFormat) + " has no sRGB view for a sampler that forces sRGB decoding");
std::lock_guard lock(srgbMutex);
auto& slot = srgbViews[baseLevel ? 2u : firstLayer ? 1u : 0u];
if (slot != VK_NULL_HANDLE) return slot;
VkImageViewCreateInfo info = *srgbInfo;
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
info.pNext = &usage;
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
if (baseLevel) {
info.viewType = info.viewType == VK_IMAGE_VIEW_TYPE_1D || info.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D;
info.subresourceRange.levelCount = 1;
info.subresourceRange.layerCount = 1;
} else {
if (firstLayer) {
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
info.subresourceRange.layerCount = 1;
}
if (srgbMinLod != 0.0f) {
minLod.minLod = srgbMinLod;
minLod.pNext = info.pNext;
info.pNext = &minLod;
}
}
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &info, nullptr, &slot), "vkCreateImageView sRGB");
return slot;
}
VkImageView Texture::BaseLevelView() const {
Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture");
std::lock_guard lock(baseLevelMutex);
@ -715,6 +871,10 @@ void Texture::release() noexcept {
firstLayerView = VK_NULL_HANDLE;
if (baseLevelView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, baseLevelView, nullptr);
baseLevelView = VK_NULL_HANDLE;
for (auto& srgbView : srgbViews) {
if (srgbView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, srgbView, nullptr);
srgbView = VK_NULL_HANDLE;
}
// The image and its memory go with the last holder: this texture, or the batch still uploading it.
owned.reset();
image = VK_NULL_HANDLE;
@ -817,7 +977,7 @@ bool IntegerFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_R8_UINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8_SINT:
case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_UINT: case VK_FORMAT_B8G8R8A8_SINT:
case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32:
case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32:
case VK_FORMAT_R16_UINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16_SINT:
case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32_SINT:
case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32_SINT:
@ -851,6 +1011,23 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc
return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear);
}
VkFormat SrgbTwinFormat(const Context& context, VkFormat format) {
const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; };
switch (format) {
case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB;
case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB;
case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB;
case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB;
case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32;
case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK;
case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK;
case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK;
case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK;
case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK;
default: return VK_FORMAT_UNDEFINED;
}
}
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) {
if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED;
struct Table {
@ -919,6 +1096,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers;
layerGeneration.assign(trackedLayers, 0);
layerPending.assign(trackedLayers, false);
layerOriginal.assign(trackedLayers, true);
sliceLinearBytes = geometry.sliceLinearBytes;
const auto linearBytes = sliceLinearBytes * arrayLayers;
@ -1333,26 +1511,26 @@ VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t
return created;
}
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layer) {
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layers, std::uint32_t layer) {
Require(attachable, "storage image cannot be a color attachment");
Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image");
const bool volume = descriptor.dimension == TextureDimension::k3D;
Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image");
Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray), "attachment slice is outside the storage image");
Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image");
const auto found = attachmentViews.find({format, mip, depthSlice, layer});
const auto found = attachmentViews.find({format, mip, depthSlice, layers, layer});
if (found != attachmentViews.end()) return found->second;
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.pNext = &usage;
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, 1u};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, layers};
VkImageView created = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment");
attachmentViews.emplace(std::tuple{format, mip, depthSlice, layer}, created);
attachmentViews.emplace(std::tuple{format, mip, depthSlice, layers, layer}, created);
return created;
}
@ -1530,6 +1708,8 @@ LookupOutcomes& ThreadLookupOutcomes() {
bool StorageTexture::Refresh() {
const bool profile = LookupOutcomes::Profiled();
auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
if (refreshProved()) {
NoteProved();
refreshesProved.fetch_add(1, std::memory_order_relaxed);
@ -1655,7 +1835,7 @@ bool StorageTexture::Refresh() {
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (changed[layer] && layerPending[layer]) pendingChanged = true;
}
if (!unchanged && !keysChanged && !pendingChanged && originalValid && equalsOriginal()) {
if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) && equalsOriginal()) {
unchanged = true;
stamped = false;
}
@ -1698,7 +1878,7 @@ bool StorageTexture::Refresh() {
static const bool traceUpload = std::getenv("APS5_TRACE_UPLOAD") != nullptr;
if (traceUpload) {
const auto keys = TextureClearKeys(descriptor, guestBytes);
std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalValid ? 1 : 0, dirty ? 1 : 0);
std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) ? 1 : 0, dirty ? 1 : 0);
}
// Changed layers with results pending: the CPU wrote this memory while GPU results were
// pending, so as with an immediate write-back followed by the CPU write, its blocks win and the
@ -1883,8 +2063,8 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std
}
void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
const bool complete = layers == nullptr || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; });
std::vector<bool> selected(trackedLayers, true);
if (layers != nullptr) selected = *layers;
for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
@ -1892,12 +2072,19 @@ void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
if (!selected[layer]) continue;
const auto offset = static_cast<std::size_t>(layerBegin(layer) - descriptor.baseAddress);
GuestMemory::ReadCommittedUnsynced(layerBegin(layer), std::span(original).subspan(offset, static_cast<std::size_t>(layerBytes(layer))));
layerOriginal[layer] = true;
}
originalValid = originalValid || complete;
}
bool StorageTexture::originalHolds(std::uint64_t address, std::size_t bytes) const {
for (auto layer = (address - descriptor.baseAddress) / trackedLayerBytes; layer < trackedLayers && layer * trackedLayerBytes < address - descriptor.baseAddress + bytes; ++layer) {
if (!layerOriginal[static_cast<std::size_t>(layer)]) return false;
}
return true;
}
bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize) const {
if (!originalValid) return false;
if (!originalHolds(address, bytes)) return false;
struct Exempt {
const StorageTexture* previous;
~Exempt() { refreshing = previous; }
@ -1906,7 +2093,7 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
Require(address >= descriptor.baseAddress && bytes <= guestBytes && address - descriptor.baseAddress <= guestBytes - bytes, "texture comparison exceeds its snapshot");
Require(original.size() == guestBytes, "texture comparison has no complete snapshot");
GuestMemory::FlushGpuWrites(address, bytes);
if (!originalValid) return false;
if (!originalHolds(address, bytes)) return false;
constexpr std::uint64_t blockBytes = 65536;
const auto end = address + bytes;
const auto first = address / blockBytes;
@ -1937,12 +2124,13 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20;
void StorageTexture::upload(const std::vector<bool>* layers) {
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
const bool profile = LookupOutcomes::Profiled();
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
const auto elementBytes = BytesPerElement(descriptor.format);
const auto linearBytes = sliceLinearBytes * arrayLayers;
original.resize(static_cast<std::size_t>(guestBytes));
// A layer selection applies to the direct path alone (the others upload everything); it names
// array layers, which the tracked layers are when there are several.
if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr;
@ -1972,7 +2160,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
// batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms
// under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before.
static_cast<void>(HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) originalValid = false;
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
else captureGuestBytes(nullptr);
forgetBorrowed(0, trackedLayers);
stampLayers(false);
@ -2021,7 +2209,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
return;
}
if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) : nullptr) {
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) originalValid = false;
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
else captureGuestBytes(layers);
stampLayers(true);
// A whole-surface upload of a block-unit image goes through the windows too when a unit
@ -2143,9 +2331,10 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start);
return;
}
originalValid = true;
layerOriginal.assign(trackedLayers, true);
forgetBorrowed(0, trackedLayers);
stampLayers(false);
original.resize(static_cast<std::size_t>(guestBytes));
GuestMemory::ReadCommitted(descriptor.baseAddress, original);
{
Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
@ -3223,9 +3412,10 @@ bool StorageTexture::evictStale(std::uint64_t address, std::size_t bytes, std::s
// As the empty write-back leaves the layers: no results pending, the generation unchanged (the
// stamps make their next Refresh re-upload), `original` no longer vouching for the bytes.
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (selected(layer)) layerPending[layer] = false;
if (!selected(layer)) continue;
layerPending[layer] = false;
layerOriginal[layer] = false;
}
originalValid = false;
if (!anyLayerPending()) {
++dropped;
staleEvicted.fetch_add(1, std::memory_order_relaxed);
@ -3285,7 +3475,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
// touched the bytes a CPU-path upload copied).
texture->dirty = false;
texture->layerPending.assign(texture->trackedLayers, false);
texture->originalValid = false;
texture->layerOriginal.assign(texture->trackedLayers, false);
it = pending.textures.erase(it);
++discarded;
} else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) {
@ -3297,13 +3487,11 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
const auto unitBegin = texture->layerBegin(unit);
if (!texture->layerPending[unit] || unitBegin < address || unitBegin + texture->layerBytes(unit) > end) continue;
texture->layerPending[unit] = false;
texture->layerOriginal[unit] = false;
unitsDropped.fetch_add(1, std::memory_order_relaxed);
dropped = true;
}
if (dropped) {
texture->originalValid = false;
++discarded;
}
if (dropped) ++discarded;
if (dropped && !texture->anyLayerPending()) {
texture->dirty = false;
it = pending.textures.erase(it);
@ -3501,7 +3689,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) {
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
recorder->EndGpuTiming(timing, guestBytes);
originalValid = false;
layerOriginal.assign(trackedLayers, false);
uploadedKeys = keys;
filledKeys = DccKeys::Uncompressed;
keyProof = {};
@ -3594,7 +3782,7 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
std::lock_guard lock(live.mutex);
for (auto* texture : live.textures) {
if (texture == this || texture->released || !texture->overlaps(begin, bytes)) continue;
texture->originalValid = false;
texture->layerOriginal.assign(texture->trackedLayers, false);
if (texture->descriptor.dccAddress != 0) aliasedKeys = true;
}
}
@ -3643,13 +3831,19 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
// memo hit of this submission's epoch would not consume pages the game dirtied since that
// walk, the write-back's own walk would stamp them above this generation and keep their
// blocks, and the fill's pattern would be lost in them.
originalValid = false;
const bool watched = GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (watched) layerOriginal.assign(trackedLayers, false);
uploadedKeys = DccKeys::Uncompressed;
if (layer != WholeImage && trackedLayers > 1) {
// The cleared layer's units alone: their stamps above lie below this generation, the other
// units keep theirs (their pages are consumed by their own collects).
const auto first = blockUnits ? static_cast<std::uint32_t>((begin - descriptor.baseAddress) / trackedLayerBytes) : layer;
const auto count = blockUnits ? static_cast<std::uint32_t>(bytes / trackedLayerBytes) : 1u;
if (!watched) {
std::vector<bool> filled(trackedLayers, false);
std::fill_n(filled.begin() + first, count, true);
captureGuestBytes(&filled);
}
const auto now = GuestMemory::CollectWritesUncached(begin, bytes);
for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = now;
refreshGeneration();
@ -3657,6 +3851,7 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
markLayersPending(first, count);
return true;
}
if (!watched) captureGuestBytes(nullptr);
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
refreshGeneration();
forgetBorrowed(0, trackedLayers);
@ -3721,7 +3916,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
auto& live = Live();
std::lock_guard lock(live.mutex);
for (auto* texture : live.textures) {
if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) texture->originalValid = false;
if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) texture->layerOriginal.assign(texture->trackedLayers, false);
}
}
GuestMemory::MarkWritten(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
@ -3772,7 +3967,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
// As after a clear: the image holds the surface, guest memory follows at the write-back (which
// keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason
// given in FillClear).
originalValid = false;
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
else captureGuestBytes(nullptr);
uploadedKeys = DccKeys::Uncompressed;
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
refreshGeneration();
@ -3999,7 +4195,7 @@ bool StorageTexture::guestBytesSettled() const {
}
bool StorageTexture::unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const {
const auto& baseline = originalValid ? original : generationBaseline;
const auto& baseline = originalHolds(from, static_cast<std::size_t>(to - from)) ? original : generationBaseline;
if (baseline.size() != guestBytes) return false;
const auto offset = static_cast<std::size_t>(from - descriptor.baseAddress);
return GuestMemory::EqualsCommittedUnsynced(from, std::span<const std::byte>(baseline).subspan(offset, static_cast<std::size_t>(to - from)));
@ -4052,12 +4248,29 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
}
std::vector<std::uint8_t> changedBlocks(spanBlocks);
const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast<std::size_t>(lastStored - firstStored), generations, changedBlocks);
const bool compared = !tracked && compareUntracked(firstStored, static_cast<std::size_t>(lastStored - firstStored), changedBlocks);
std::vector<bool> compared(trackedLayers, false);
if (!tracked) {
std::vector<bool> saved(trackedLayers, false);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) saved[layer] = layers[layer] && layerOriginal[layer];
for (const auto& [begin, end] : unitRuns(saved)) {
const auto from = descriptor.baseAddress + begin;
const auto last = descriptor.baseAddress + end - 1;
if (!compareUntracked(from, static_cast<std::size_t>(end - begin), std::span(changedBlocks).subspan(static_cast<std::size_t>((from - spanBegin) / block), static_cast<std::size_t>(last / block - from / block + 1)))) continue;
for (auto layer = static_cast<std::uint32_t>(begin / trackedLayerBytes); layer < trackedLayers && layerBegin(layer) <= last; ++layer) compared[layer] = true;
}
}
const auto forgetStored = [&] {
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer]) layerOriginal[layer] = false;
}
};
bool unsavedOutsideWatch = false;
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!layers[layer]) continue;
const auto begin = layerBegin(layer);
const auto end = begin + layerBytes(layer);
if (!compared && (!tracked || layerGeneration[layer] == 0)) {
if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) {
unsavedOutsideWatch = unsavedOutsideWatch || !tracked;
keep.emplace_back(begin, end);
continue;
}
@ -4066,7 +4279,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
const auto to = std::min(at + block, end);
if (from >= to) continue;
const bool edge = from != at || to != at + block;
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) {
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) {
skippedAny = true;
skippedLayer[layer] = true;
++skipped;
@ -4118,18 +4331,19 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
// store, the keys stay as they are, and the image is stale there (re-uploaded at its next
// use, never from a matching `original`).
emptyWriteBacks.fetch_add(1, std::memory_order_relaxed);
originalValid = false;
forgetStored();
settle(true);
return;
}
if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) {
Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented");
if (blockUnits) {
// The kept blocks alone pass through the retiler (windows of their slices).
shadowImport = import;
const auto storedBytes = writeBackWindows(*import, keep, firstStored, lastStored, shadowed, imported);
countStorageWriteBack(storedBytes, true);
if (profile) Profile().storageGpu += timer.lap();
originalValid = false;
forgetStored();
traceKeyStore("block write-back", descriptor, guestBytes);
if (!IsDccClear(filledKeys) && uploadedKeys != DccKeys::Uncompressed) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes));
uploadedKeys = DccKeys::Uncompressed;
@ -4234,7 +4448,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
countStorageWriteBack(storedBytes, true);
if (profile) Profile().storageGpu += timer.lap();
// The guest bytes now differ from `original`; other caches of the range see the write.
originalValid = false;
forgetStored();
// The keys go the same way as the texels: a fill recorded behind the retile when the
// metadata is host-imported (no CPU wait for the title's key-writing kernels), else a CPU
// store (APS5_CPU_DCC_KEYS=1 keeps the CPU store; see DccMetadata.hpp).
@ -4258,18 +4472,21 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
static std::atomic<int> reports{0};
if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), keep.size());
unregisteredDropped.fetch_add(1, std::memory_order_relaxed);
originalValid = false;
forgetStored();
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer]) layerPending[layer] = false;
}
return;
}
original.resize(static_cast<std::size_t>(guestBytes));
// The store below compares against the guest bytes, read now when `original` does not hold
// them (a GPU clear, a dropped or kept unit). Reading them says nothing about the image: a
// stale unit (dropped, or kept for the CPU) still differs from them, so `original` vouches for
// the content again only where it did before, or once every unit is stored.
const bool wasValid = originalValid;
if (!wasValid) GuestMemory::ReadCommitted(descriptor.baseAddress, original);
Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented");
std::vector<bool> unsaved = layerOriginal;
unsaved.flip();
for (const auto& [begin, end] : unitRuns(unsaved)) GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast<std::size_t>(begin), static_cast<std::size_t>(end - begin)));
DeviceBuffer linear(context, static_cast<std::size_t>(sliceLinearBytes * arrayLayers), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
Buffer host(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
@ -4353,7 +4570,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
uploadedKeys = DccKeys::Uncompressed;
// The store above is the only write to these pages, so `original` is current at a fresh
// generation, unless blocks were kept for the CPU: then the image is stale there.
originalValid = !skippedAny && (wasValid || std::all_of(layers.begin(), layers.end(), [](bool selected) { return selected; }));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer]) layerOriginal[layer] = !skippedLayer[layer];
}
// As in the GPU-direct path, but the store's memcpy dirtied this surface's pages in the write
// watch: a memoized collect would leave them for the next walk to stamp newer than this value
// (the shared edge block included, undoing the advance), so the walk is made here and

View File

@ -181,7 +181,10 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
createInfo.stage = stage;
createInfo.layout = image ? imagePipelineLayout : pipelineLayout;
VkPipeline result = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
}
pipelines.emplace_back(key, result);
return result;
}
@ -299,7 +302,11 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask
pipelineInfo.stage.module = cmaskModule;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = cmaskPipelineLayout;
const auto created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
}
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, cmaskModule, nullptr);
Check(created, "vkCreateComputePipelines CMASK clear");
cmaskErrors = std::make_unique<Buffer>(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);

View File

@ -42,7 +42,9 @@ constexpr FormatEntry kFormatLookup[] = {
{29, VK_FORMAT_R16G16_SFLOAT, 4, false},
{36, VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4, false},
{50, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, false},
{51, VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4, false},
{54, VK_FORMAT_A2B10G10R10_UINT_PACK32, 4, false},
{55, VK_FORMAT_A2B10G10R10_SINT_PACK32, 4, false},
{56, VK_FORMAT_R8G8B8A8_UNORM, 4, false},
{57, VK_FORMAT_R8G8B8A8_SNORM, 4, false},
{60, VK_FORMAT_R8G8B8A8_UINT, 4, false},

View File

@ -10,25 +10,25 @@ add_custom_command(
VERBATIM
)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_mesh_tests)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp ${agcColorTransferHeader})
endif()
endforeach()
target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/TextureTiling.cpp Graphics/src/TextureFormat.cpp Execution/src/GuestMemory.cpp)
target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp)
target_include_directories(agc_driver_bda_device_tests PRIVATE ${agcTextureDetileGeneratedRoot})
if(TARGET agc_driver_visual_test)

View File

@ -175,6 +175,42 @@ void failedImportRemapTests(const Context& context, const BdaTestAccess& access)
::operator delete(block, std::align_val_t{piece});
}
void spaceImportRunTests(const Context& context) {
constexpr std::size_t piece = 65536;
void* block = ::operator new(2 * piece, std::align_val_t{piece});
const auto first = reinterpret_cast<std::uintptr_t>(block);
std::memset(block, 0x3c, 2 * piece);
auto importing = context;
importing.hostImportAlignment = piece;
const auto change = [&](std::uint8_t* range, bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
auto* guest = static_cast<std::uint8_t*>(block);
change(guest, true);
change(guest + piece, true);
for (int build = 0; build < 2; ++build) {
const auto before = AddressSpaceCounters();
GuestBufferMemory leased(importing);
leased.AcquireRegistered();
leased.AddReadable(first + piece - 256, 512);
leased.Upload(true);
std::uint32_t adjustment = 0;
const auto view = leased.Descriptor(first + piece - 256, 512, adjustment);
Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a V# across one import's ranges has no descriptor");
const auto ranges = leased.AddressRanges();
Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && first + 2 * piece <= range.end; }), "one import's adjacent ranges are not one BDA table entry");
leased.WriteBack();
const auto after = AddressSpaceCounters();
if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a V# across one import's ranges dissolved the cached address space");
}
change(guest, false);
change(guest + piece, false);
::operator delete(block, std::align_val_t{piece});
}
void importWindowTests(const Context& context) {
constexpr std::size_t piece = 65536;
constexpr std::size_t window = 2 * piece;
@ -207,6 +243,237 @@ void importWindowTests(const Context& context) {
::operator delete(block, std::align_val_t{window});
}
void importJoinTests(const Context& context) {
constexpr std::size_t piece = 65536;
constexpr std::size_t window = 2 * piece;
constexpr std::size_t large = 3 * piece;
void* block = ::operator new(3 * large + 5 * piece, std::align_val_t{window});
auto* guest = static_cast<std::uint8_t*>(block);
std::memset(guest, 0x11, 3 * large);
const auto first = reinterpret_cast<std::uintptr_t>(block);
const auto second = first + large;
const auto third = second + large;
auto importing = context;
importing.hostImportAlignment = piece;
SetHostImportWindow(window);
const auto change = [&](std::uint8_t* range, std::size_t bytes, bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, bytes, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
const auto bind = [&](std::uintptr_t address, std::size_t bytes = 32) {
GuestBufferMemory plain(importing);
plain.AddReadable(address, bytes);
plain.Upload(false);
std::uint32_t adjustment = 0;
const auto view = plain.Descriptor(address, bytes, adjustment);
const auto copied = plain.CopiedBytes();
plain.WriteBack();
return std::pair{view.buffer, copied};
};
change(guest, large, true);
change(guest + large, large, true);
const auto* low = HostImportFor(importing, first, 16);
Require(low != nullptr && low->base == first && low->bytes == large, "a range larger than the import window was not imported alone");
const auto* high = HostImportFor(importing, second, 16);
Require(high != nullptr && high->base == second && high->bytes == large, "the next range larger than the import window was not imported alone");
Require(HostImportFor(importing, second - 16, 32) == nullptr, "a view across two imported ranges was imported before it was bound");
{
GuestBufferMemory leased(importing);
leased.AcquireRegistered();
leased.AddReadable(second - 16, 32);
leased.Upload(true);
const auto* joined = HostImportFor(importing, second - 16, 32);
Require(joined != nullptr && joined->base == first && joined->bytes == 2 * large, "a buffer bound across two adjacent ranges larger than the import window was not imported as one");
std::uint32_t adjustment = 0;
const auto crossing = leased.Descriptor(second - 16, 32, adjustment);
Require(crossing.buffer == joined->buffer && crossing.offset + adjustment == second - 16 - first, "a buffer bound across the joined ranges is not bound in place");
leased.WriteBack();
}
const auto joinedBuffer = HostImportFor(importing, first, 16)->buffer;
const auto [again, copied] = bind(second - 16);
Require(again == joinedBuffer && copied == 0, "a later bind across the joined ranges did not use the joined import");
auto* heap = guest + 3 * large + 2 * piece;
change(heap, piece, true);
change(heap + piece, piece, true);
Require(bind(reinterpret_cast<std::uintptr_t>(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast<std::uintptr_t>(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined");
const auto heapBase = reinterpret_cast<std::uintptr_t>(heap);
const auto [piecesBuffer, piecesCopied] = bind(heapBase, 2 * piece);
const auto* pieces = HostImportFor(importing, heapBase, 2 * piece);
Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one");
change(heap + 2 * piece, piece, true);
static_cast<void>(bind(heapBase + 2 * piece - 16));
const auto* kept = HostImportFor(importing, first, 16);
Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges");
change(guest + 2 * large, large, true);
Require(HostImportFor(importing, first, 16)->buffer == joinedBuffer, "a range registered next to the joined ranges re-imported them");
const auto [grownBuffer, grownCopied] = bind(third - 16);
const auto* grown = HostImportFor(importing, third - 16, 32);
Require(grown != nullptr && grown->base == first && grown->bytes == 3 * large && grownBuffer == grown->buffer && grownCopied == 0, "a bind across the next range did not grow the joined import");
Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other");
change(heap, piece, false);
change(heap + piece, piece, false);
change(heap + 2 * piece, piece, false);
change(guest + large, large, false);
const auto* alone = HostImportFor(importing, first, 16);
Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges");
change(guest, large, false);
change(guest + 2 * large, large, false);
Require(HostImportFor(importing, first, 16) == nullptr && HostImportFor(importing, third, 16) == nullptr, "the host imports outlived their ranges");
SetHostImportWindow(0);
::operator delete(block, std::align_val_t{window});
}
// One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the
// test and reset after it.
class WindowedRange {
public:
WindowedRange(const Context& context, std::size_t window, std::size_t cells, std::uint64_t budget = 0) : window(window), bytes(window * cells), importing(context) {
block = ::operator new(bytes, std::align_val_t{piece});
std::memset(block, 0x33, bytes);
importing.hostImportAlignment = piece;
SetHostImportRangeWindow(window);
SetHostImportBudget(budget);
change(true);
}
~WindowedRange() {
change(false);
SetHostImportBudget(0);
SetHostImportRangeWindow(std::uint64_t{64} << 20u);
::operator delete(block, std::align_val_t{piece});
}
WindowedRange(const WindowedRange&) = delete;
WindowedRange& operator=(const WindowedRange&) = delete;
std::uintptr_t First() const { return reinterpret_cast<std::uintptr_t>(block); }
static constexpr std::size_t piece = 65536;
const std::size_t window;
const std::size_t bytes;
Context importing;
private:
void change(bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
}
void* block = nullptr;
};
// A registered range larger than the import window, its first window imported by an earlier access: an address-based
// build (AcquireRegistered) must import the whole range, so a view in its last window binds (Demon's Souls threw
// "guest buffer view ... exceeds its GPU owner" with the 64 MiB windows).
void importWholeRangeOverWindowTests(const Context& context) {
WindowedRange range(context, 2 * WindowedRange::piece, 2);
const auto first = range.First();
const auto* windowed = HostImportFor(range.importing, first + 16, 32);
Require(windowed != nullptr && windowed->base == first && windowed->bytes == range.window, "an access to the first window of a large range was not imported as that window");
GuestBufferMemory leased(range.importing);
leased.AcquireRegistered();
leased.Upload(true);
std::uint32_t adjustment = 0;
const auto view = leased.Descriptor(first + range.bytes - WindowedRange::piece + 16, 32, adjustment);
Require(view.range == 32 + adjustment, "a view in the last window of a large registered range has no descriptor");
const auto* whole = HostImportFor(range.importing, first, range.bytes);
Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && view.buffer == whole->buffer, "an address-based build was served by the range's first window, not an import of the whole range");
leased.WriteBack();
}
// An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before,
// every such build retired the window and the next access imported it again.
void importRefusedWholeRangeTests(const Context& context) {
WindowedRange range(context, 2 * WindowedRange::piece, 4, 3 * 2 * WindowedRange::piece);
const auto first = range.First();
const auto* windowed = HostImportFor(range.importing, first + 16, 32);
Require(windowed != nullptr && windowed->bytes == range.window, "the first window of a range over the budget was not imported");
const auto buffer = windowed->buffer;
for (int build = 0; build < 3; ++build) {
GuestBufferMemory leased(range.importing);
leased.AcquireRegistered();
leased.Upload(true);
leased.WriteBack();
Require(HostImportCovers(range.importing, first + 16, 32), "an address-based build refused its whole-range import and retired the range's window (build " + std::to_string(build) + ")");
}
const auto* kept = HostImportFor(range.importing, first + 16, 32);
Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import");
}
// Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows
// the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at
// every use.
void importNeighbouringEdgesTests(const Context& context) {
WindowedRange range(context, 2 * WindowedRange::piece, 3);
const auto first = range.First();
const auto use = [&](std::uintptr_t address) {
GuestBufferMemory leased(range.importing);
leased.AddReadable(address, 32);
leased.Upload(false);
leased.WriteBack();
};
const auto acrossFirst = first + range.window - 16;
const auto acrossSecond = first + 2 * range.window - 16;
use(acrossFirst);
Require(HostImportCovers(range.importing, acrossFirst, 32), "a use across the first cell edge was not imported");
use(acrossSecond);
Require(HostImportCovers(range.importing, first, range.bytes), "a use across the second cell edge did not grow the first use's window to their union");
const auto serial = HostImportSerial(range.importing, first, range.bytes, false);
for (int round = 0; round < 10; ++round) {
use(acrossFirst);
use(acrossSecond);
Require(HostImportSerial(range.importing, first, range.bytes, false) == serial, "a use across a cell edge replaced the shared window (round " + std::to_string(round) + ")");
}
}
// A range mapped while the imports are at the budget is refused at map time without retiring an import: the map
// observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit).
void importAtMapOverBudgetTests(const Context& context) {
constexpr std::size_t bytes = 65536;
auto importing = context;
importing.hostImportAlignment = bytes;
void* kept = ::operator new(bytes, std::align_val_t{bytes});
void* used = ::operator new(bytes, std::align_val_t{bytes});
void* mapped = ::operator new(bytes, std::align_val_t{bytes});
const auto change = [&](void* block, bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
change(kept, true);
change(used, true);
const auto keptAddress = reinterpret_cast<std::uintptr_t>(kept);
const auto usedAddress = reinterpret_cast<std::uintptr_t>(used);
Require(HostImportFor(importing, keptAddress, 16) != nullptr, "the import to keep was not made");
for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, usedAddress, 16) != nullptr, "the import in use was not made");
{
GuestBufferMemory leased(importing);
leased.AddReadable(usedAddress, 16);
leased.Upload(false);
leased.WriteBack();
}
const auto live = HostImportLiveBytes();
SetHostImportBudget(live + bytes - 1);
change(mapped, true);
GuestAllocations::Mapped ranges;
{
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
for (const auto& range : lease) {
if (range->address == reinterpret_cast<std::uintptr_t>(mapped)) ranges.push_back(range);
}
}
Require(ranges.size() == 1, "the mapped range is not registered");
static_cast<void>(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true));
SetHostImportBudget(0);
Require(HostImportLiveBytes() == live, "a range mapped at the budget retired an import at map time (an eviction without the GPU lock)");
Require(HostImportCovers(importing, keptAddress, 16) && !HostImportCovers(importing, reinterpret_cast<std::uintptr_t>(mapped), 16), "a range mapped at the budget was imported, or the import to keep was retired");
for (auto* block : {kept, used, mapped}) change(block, false);
for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes});
}
ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) {
return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false};
}
@ -1202,7 +1469,13 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
importCrossingTests(context, access);
importSpanTests(context);
failedImportRemapTests(context, access);
spaceImportRunTests(context);
importWindowTests(context);
importJoinTests(context);
importWholeRangeOverWindowTests(context);
importRefusedWholeRangeTests(context);
importNeighbouringEdgesTests(context);
importAtMapOverBudgetTests(context);
heapMirrorTests(context, access);
importedHeapMirrorTests(context, access);
importWatchTests();

View File

@ -145,22 +145,14 @@ void run(AgcDriver::VulkanDevice& device, ColorTileMode mode, const std::string&
expect(device, color, [](std::uint32_t x, std::uint32_t y) { return y < 4 ? Drawn : y >= 8 && x >= 24 ? Clear : Stale; }, name + " fast-cleared bottom edge tiles under a draw of the top rows");
}
void invalidCode(AgcDriver::VulkanDevice& device) {
void unknownCode(AgcDriver::VulkanDevice& device) {
const auto color = target(ColorTileMode::RenderTarget);
prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 2 && tileY == 0; }, 3);
eliminate(device, color);
{
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
device.WaitIdle();
}
expect(device, color, [](std::uint32_t, std::uint32_t) { return Stale; }, "a tile with CMASK code 3 eliminated");
prepare(color, [](std::uint32_t, std::uint32_t) { return true; });
try {
eliminate(device, color);
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find("were recorded, which are not a single-sample fast-clear code") != std::string::npos, std::string("unexpected failure: ") + error.what());
return;
}
throw std::runtime_error("a CMASK code 3 read by the GPU pass did not throw");
eliminate(device, color);
expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, "fast-cleared target eliminated after a CMASK code 3");
}
}
@ -182,7 +174,7 @@ int main() {
}
run(*device, ColorTileMode::Linear, "linear");
run(*device, ColorTileMode::RenderTarget, "SW_64KB_R_X");
invalidCode(*device);
unknownCode(*device);
device->WaitIdle();
device.reset();
{

View File

@ -150,6 +150,66 @@ int main() {
return b.eliminate() == 1u && !clamped.HasUses() && product.HasUses();
});
passed &= run("a min, max, min3, max3 or med3 of flushed values needs no flush, one with an unknown operand does", [] {
Builder b;
const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); };
const auto flushedResult = [&](IrOpcode opcode, std::initializer_list<IrValue*> arguments) -> IrValue& {
return b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(opcode, IrType::F32, arguments)}));
};
auto& x = value(b.flush(b.unknown()));
auto& y = value(b.flush(b.unknown()));
auto& z = value(b.flush(b.unknown()));
auto& raw = value(b.unknown());
IrValue* removed[] = {
&flushedResult(IrOpcode::FPMin32, {&x, &y}),
&flushedResult(IrOpcode::FPMax32, {&y, &x}),
&flushedResult(IrOpcode::FPMinTri32, {&x, &y, &z}),
&flushedResult(IrOpcode::FPMaxTri32, {&z, &y, &x}),
&flushedResult(IrOpcode::FPMedTri32, {&x, &z, &y}),
};
auto& mixedPair = flushedResult(IrOpcode::FPMax32, {&x, &raw});
auto& mixedMedian = flushedResult(IrOpcode::FPMedTri32, {&x, &y, &raw});
for (auto* flushed : removed) b.keep(*flushed);
b.keep(mixedPair);
b.keep(mixedMedian);
const bool counted = b.eliminate() == 5u;
bool kept = false;
for (auto* flushed : removed) kept |= flushed->HasUses();
return counted && !kept && mixedPair.HasUses() && mixedMedian.HasUses();
});
passed &= run("a floor or ceil needs no flush, even of an unknown value", [] {
Builder b;
auto& raw = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()});
auto& floored = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPFloor32, IrType::F32, {&raw})}));
auto& ceiled = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPCeil32, IrType::F32, {&raw})}));
b.keep(floored);
b.keep(ceiled);
return b.eliminate() == 2u && !floored.HasUses() && !ceiled.HasUses();
});
passed &= run("a max carried around a loop without a flush keeps its flush and the search ends", [] {
Builder b;
IrBlock& head = *b.block;
IrBlock& body = b.newBlock();
IrBlock& exit = b.newBlock();
head.AddBranch(&body);
body.AddBranch(&body);
body.AddBranch(&exit);
auto& initial = b.flush(b.unknown());
b.block = &body;
auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32);
body.AppendInstruction(&carried);
auto& step = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())});
auto& maximum = b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMax32, IrType::F32, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&carried}), &step})});
carried.AddPhiOperand(&head, &initial);
carried.AddPhiOperand(&body, &maximum);
auto& flushed = b.flush(maximum);
b.block = &exit;
b.keep(flushed);
return b.eliminate() == 0u && flushed.HasUses();
});
passed &= run("a phi of flushed values needs no flush", [] {
Builder b;
IrBlock& head = *b.block;

View File

@ -230,6 +230,7 @@ void RunDepthSurfaceReuseTests() {
context.memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
context.limits.maxFramebufferWidth = 16384;
context.limits.maxFramebufferHeight = 16384;
context.limits.maxFramebufferLayers = 1;
TestD16StencilFallback(context);
constexpr std::uint64_t depth32 = 0x40000000;
constexpr std::uint64_t depth16 = 0x50000000;

View File

@ -0,0 +1,6 @@
#include <cstdio>
int main() {
std::puts("skipped, this build leaves the test out with ANYPS5_AGC_EXECUTION_TESTS");
return 77;
}

View File

@ -0,0 +1,119 @@
#include "BdaAbi.hpp"
#include "Recompiler.hpp"
#include <array>
#include <cstdio>
#include <exception>
#include <span>
#include <spirv/unified1/spirv.hpp>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace {
using namespace ShaderRecompiler;
void Check(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
SpirvTarget Target(bool byteAccess) {
static constexpr std::array<std::uint32_t, 3> withBytes{
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess,
};
static constexpr std::array<std::uint32_t, 2> withoutBytes{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses};
static constexpr std::array<std::string_view, 4> extensions{
"SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer",
};
return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, byteAccess ? std::span<const std::uint32_t>(withBytes) : std::span<const std::uint32_t>(withoutBytes), extensions, false,
{1024, 1024, 64}, 1024, 32768, {}, {}};
}
struct Counts {
std::size_t compareExchanges = 0;
std::size_t byteStores = 0;
std::size_t byteTypes = 0;
bool byteCapability = false;
bool byteBuffers = false;
};
Counts Count(std::span<const std::uint32_t> words) {
Check(words.size() >= 5 && words[0] == spv::MagicNumber, "formatted buffer byte stores: missing SPIR-V module");
Counts counts;
std::uint32_t byteType = 0;
std::vector<std::uint32_t> bytePointers;
for (std::size_t offset = 5; offset < words.size();) {
const auto count = words[offset] >> spv::WordCountShift;
Check(count != 0 && count <= words.size() - offset, "formatted buffer byte stores: malformed SPIR-V instruction");
const auto opcode = words[offset] & spv::OpCodeMask;
if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges;
if (opcode == spv::OpCapability && words[offset + 1] == spv::CapabilityStorageBuffer8BitAccess) counts.byteCapability = true;
if (opcode == spv::OpTypeInt && words[offset + 2] == 8u) {
++counts.byteTypes;
byteType = words[offset + 1];
}
if (opcode == spv::OpTypePointer && words[offset + 2] == spv::StorageClassStorageBuffer && words[offset + 3] == byteType && byteType != 0) bytePointers.push_back(words[offset + 1]);
if (opcode == spv::OpName) {
const auto name = std::string_view(reinterpret_cast<const char*>(&words[offset + 2]));
if (name == "buffers_u8") counts.byteBuffers = true;
}
offset += count;
}
std::vector<std::uint32_t> byteChains;
for (std::size_t offset = 5; offset < words.size();) {
const auto count = words[offset] >> spv::WordCountShift;
const auto opcode = words[offset] & spv::OpCodeMask;
if (opcode == spv::OpAccessChain) {
for (const auto pointer : bytePointers) {
if (words[offset + 1] == pointer) byteChains.push_back(words[offset + 2]);
}
}
if (opcode == spv::OpStore) {
for (const auto chain : byteChains) {
if (words[offset + 1] == chain) ++counts.byteStores;
}
}
offset += count;
}
return counts;
}
Counts Compile(bool byteAccess, bool vertex = false) {
std::vector<std::uint32_t> code{0xe0000000u, 0x80020a00u, 0xbf8c3f70u, 0xe0100000u, 0x80020a00u};
if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u});
code.push_back(0xbf810000u);
const std::span<const std::uint32_t> words(code);
const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(words)}}};
const std::array<std::uint32_t, 12> userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu};
RecompileRequest request{};
request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), words, 0, {}};
request.target = Target(byteAccess);
request.layout = {0, 0, 0, 128};
request.context.waveSize = 32;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
if (vertex) request.context.vertex = ShaderVertexStageInfo{};
else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.context.memory = memory;
request.useCache = false;
return Count(Recompile(request).spirv.Words());
}
}
int main() {
try {
const auto bytes = Compile(true);
Check(bytes.compareExchanges == 0, "formatted buffer byte stores: a store still patches its dword with " + std::to_string(bytes.compareExchanges) + " compare-exchange loops");
Check(bytes.byteBuffers && bytes.byteCapability && bytes.byteTypes == 1, "formatted buffer byte stores: the byte view of the buffers is not declared under StorageBuffer8BitAccess");
Check(bytes.byteStores >= 4, "formatted buffer byte stores: the 8-bit components are stored with " + std::to_string(bytes.byteStores) + " byte stores, expected at least 4");
const auto dwords = Compile(false);
Check(dwords.compareExchanges > 0 && !dwords.byteBuffers && !dwords.byteCapability, "formatted buffer byte stores: without StorageBuffer8BitAccess the stores do not fall back to the dword patch");
std::printf("formatted buffer byte stores tests passed (%zu byte stores, %zu compare-exchange loops without the capability)\n", bytes.byteStores, dwords.compareExchanges);
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -7,6 +7,7 @@
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
@ -22,6 +23,7 @@
#include "BdaAbi.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/GuestHeap.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <array>
@ -51,6 +53,7 @@ using AgcDriver::Graphics::Require;
alignas(256) std::array<std::byte, 1024> colorMemory{};
alignas(256) std::array<std::byte, 2048> sliceMemory{};
alignas(256) std::array<std::uint8_t, 4> volumeKeyMemory{};
AgcDriver::QueueState makeState() {
AgcDriver::QueueState queue;
@ -98,6 +101,90 @@ void expectFailure(TAction action, std::string_view reason) {
throw std::runtime_error("expected graphics rejection: " + std::string(reason));
}
void inactiveVertexPointSizeTests() {
const auto pointSizeState = [] {
auto queue = makeState();
queue.context[0x1b3] = queue.context[0x1b4] = 2;
queue.context[0x207] = 0x01210000;
return queue;
};
for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) {
auto queue = pointSizeState();
queue.userConfig[0x242] = primitive;
for (const auto raster : {0u, 0x240u, 0x248u}) {
queue.context[0x205] = raster;
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
Require(rejection.empty(), "inactive vertex point size failed the draw precheck: " + rejection);
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
}
}
for (const auto raster : {0u, 0x240u, 0x248u}) {
auto queue = pointSizeState();
queue.userConfig[0x242] = 3;
queue.context[0x205] = raster;
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
Require(rejection.empty(), "inactive line-strip point size failed the draw precheck: " + rejection);
const auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.primitiveRestart, "inactive point size changed line-strip decoding");
Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "inactive point size changed the line primitive class");
queue.userConfig[0x24b] = 1;
queue.context[0x103] = 0xffffffffu;
const auto indexedRejection = AgcDriver::Graphics::DrawRejection(queue, true);
Require(indexedRejection.empty(), "inactive line-strip point size with primitive restart failed the draw precheck: " + indexedRejection);
const auto indexedState = AgcDriver::Graphics::DecodeState(queue);
Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && indexedState.primitiveRestart, "inactive point size changed indexed line-strip decoding");
}
const auto rejected = [](AgcDriver::QueueState queue) {
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("auxiliary vertex exports") != std::string::npos, "unsupported vertex exports passed the draw precheck");
try {
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
} catch (const std::runtime_error&) {
return;
}
throw std::runtime_error("potentially active point size passed the state decoder");
};
for (const auto primitive : {1u, 9u}) {
auto queue = pointSizeState();
queue.userConfig[0x242] = primitive;
rejected(queue);
}
for (const auto stages : {0x2004u, 0x2020u}) {
auto queue = pointSizeState();
queue.context[0x2d5] = stages;
rejected(queue);
}
for (const auto raster : {8u, 0x208u, 0x48u}) {
auto queue = pointSizeState();
queue.context[0x205] = raster;
rejected(queue);
}
for (const auto flag : {1u << 17u, 1u << 20u}) {
auto queue = pointSizeState();
queue.context[0x207] = 0x01210000 | flag;
rejected(queue);
}
for (const auto distances : {1u, 0x100u, 0x00c000ffu, 0x00c0ff00u}) {
auto queue = pointSizeState();
queue.context[0x207] |= distances;
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "inactive point size rejected supported clip/cull distances");
Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == queue.context[0x207], "inactive point size discarded clip/cull distance state");
}
{
auto queue = pointSizeState();
queue.context[0x207] |= 0x004001ffu;
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "inactive point size bypassed the clip/cull count limit");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances");
}
for (const auto offset : {0x2d5u, 0x205u}) {
auto queue = pointSizeState();
queue.context.erase(offset);
rejected(queue);
}
auto queue = pointSizeState();
queue.userConfig.erase(0x242);
rejected(queue);
}
void hdrComponentSwapTests() {
using namespace AgcDriver::Graphics;
for (const auto number : {0u, 1u, 4u, 7u}) {
@ -451,6 +538,58 @@ void hardwareScreenOffsetTests() {
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
}
void UnwrittenSampleLocationTests() {
using AgcDriver::Graphics::DecodeState;
using AgcDriver::Graphics::DrawRejection;
using AgcDriver::Graphics::SampleLocations;
const auto fresh = [](std::uint32_t config, std::uint32_t attributes, std::uint32_t scanMode = 3) {
auto written = makeState();
written.context[0x2f8] = config;
written.context[0x292] = scanMode;
written.context[0x3b8] = 0x0dc00000u;
written.context[0x31d] = attributes;
written.context[0x1b3] = 2;
written.context[0x1b4] = 2;
AgcDriver::QueueState queue;
queue.shader = written.shader;
queue.userConfig = written.userConfig;
written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; });
return queue;
};
AgcDriver::QueueState initial;
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(initial.context.contains(offset) && initial.context.at(offset) == 0, "an unwritten PA_SC_AA_SAMPLE_LOCS register did not hold its clear-state value 0");
for (const auto samples : {2u, 4u, 8u}) {
const auto log2 = static_cast<std::uint32_t>(std::countr_zero(samples));
auto queue = fresh((log2 << 20u) | log2, (log2 << 12u) | (log2 << 15u));
const auto state = DecodeState(queue);
Require(state.samples == samples && state.colors.size() == 1 && state.colors[0].samples == samples, "a multisampled draw with unwritten sample locations did not decode its sample count");
Require(state.customSampleLocations, "unwritten sample locations did not decode as the clear-state pattern");
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(state.sampleLocations[sample] == 0, "an unwritten sample location was not at the pixel center");
const auto centered = SampleLocations(state);
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "unwritten sample locations did not place every sample at the pixel center");
const auto rejection = DrawRejection(queue, false);
Require(rejection.empty(), "the precheck refused a multisampled draw with unwritten sample locations: " + rejection);
}
auto queue = fresh(0x00200002, 0x12000);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae;
auto state = DecodeState(queue);
Require(state.samples == 4 && !state.customSampleLocations, "explicit standard 4x sample locations changed");
queue = fresh(0x00200002, 0x12000);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a;
state = DecodeState(queue);
Require(state.samples == 4 && state.customSampleLocations, "explicit custom 4x sample locations changed");
const auto custom = SampleLocations(state);
const std::array<VkSampleLocationEXT, 4> expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}};
for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(custom[sample].x == expected[sample].x && custom[sample].y == expected[sample].y, "explicit custom 4x sample coordinates changed");
queue.ClearContext();
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(queue.context.at(offset) == 0, "CLEAR_STATE did not reset PA_SC_AA_SAMPLE_LOCS");
expectFailure([&] { DecodeState(fresh(0x00400004, 0x12000)); }, "more than eight samples");
expectFailure([&] { DecodeState(fresh(0x00100002, 0x12000)); }, "exposing fewer samples");
queue = fresh(0, 0, 3);
state = DecodeState(queue);
Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused");
}
void MsaaEnableSingleSampleTests() {
using AgcDriver::Graphics::DecodeState;
using AgcDriver::Graphics::DrawRejection;
@ -572,6 +711,34 @@ void ShaderStageTests() {
fan.userConfig[0x242] = 3;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "unsupported geometry input or output assembly");
}
{
auto fast = makeState();
fast.userConfig[0x242] = 1;
fast.context[0x2d5] = 0x482030;
fast.userConfig[0x25b] = 0x201;
fast.context[0x1ff] = 32;
fast.context[0x2ce] = 32;
fast.context[0x2d3] = 64;
fast.context[0x29b] = 2;
fast.context[0x2ab] = 4;
fast.shader[0x8a] = 0x022c0245;
fast.shader[0x8b] = 0x9002a;
const auto state = AgcDriver::Graphics::DecodeState(fast);
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry");
const auto& mesh = *state.stages.mesh;
Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR");
fast.context[0x2d5] = 0x502030;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state");
fast.context[0x2d5] = 0x482030;
fast.userConfig[0x25b] = 0x402;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
fast.userConfig[0x25b] = 0x201;
fast.userConfig[0x242] = 4;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
fast.userConfig[0x242] = 1;
fast.context[0x2d3] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives");
}
queue.context[0x2ab] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE");
queue.context[0x2ab] = 4;
@ -598,17 +765,28 @@ void ShaderStageTests() {
}
Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed");
const auto& tessellation = *stages.tessellation;
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2, "the passthrough triangle-patch configuration decoded differently");
for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "only triangular"}, std::pair{0x40069u, "only triangular"}, std::pair{0x4006au, "only triangular"}}) {
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently");
for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) {
queue.context[0x2db] = parameters;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
}
queue.context[0x2db] = 0x40049;
for (const auto [maximum, minimum] : {std::pair{0x41100000u, 0x3f800000u}, std::pair{0x42800000u, 0x40000000u}}) {
queue.context[0x2db] = 0x40041;
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently");
queue.context[0x2d6] = 0x1043f;
queue.context[0x2db] = 0x4006a;
queue.context[0x286] = 0x41100000;
const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages;
const auto& quads = quadStages.tessellation;
Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63, "counter-clockwise quad patches with HOS levels [1, 9] and 63 patches per group decoded differently");
queue.context[0x2db] = 0x40069;
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently");
for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) {
queue.context[0x286] = maximum;
queue.context[0x287] = minimum;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "tessellation level clamps");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
}
queue.context[0x2d6] = 0xc355;
queue.context[0x2db] = 0x40049;
queue.context[0x286] = 0x42800000;
queue.context[0x287] = 0x3f800000;
queue.context[0x2d5] = 0x202d;
@ -616,6 +794,8 @@ void ShaderStageTests() {
queue.context[0x2d5] = 0x0200210d;
queue.context[0x2d6] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "control-point counts");
queue.context[0x2d6] = 0xc300;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NUM_PATCHES is zero");
queue = makeState();
for (const auto value : {0x2003u, 0x2018u, 0x20c0u, 0x80002000u}) {
queue.context[0x2d5] = value;
@ -964,6 +1144,16 @@ void PixelInputLayoutTests() {
Require(!readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "an ADDR-only centroid pair was loaded");
}
void BlendBypassTests() {
auto queue = makeState();
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
queue.context[0x1e0] = (1u << 30u) | 4u | (5u << 8u);
Require(AgcDriver::Graphics::DecodeState(queue).blends.at(0).blendEnable, "enabled blending was not decoded");
queue.context[0x31c] |= 0x10000u;
const auto bypassed = AgcDriver::Graphics::DecodeState(queue);
Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends");
}
void OpaqueDestinationAlphaTests() {
auto queue = makeState();
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
@ -1332,6 +1522,12 @@ void DepthStencilTests() {
Require(plainRange.color.address == plain.color.address && plainRange.color.bytes == plain.color.bytes && plainRange.color.arrayLastSlice == plain.color.arrayLastSlice, "a color view of slices 0 to 35 of a target with no array slices did not render into its only slice");
queue.context[0x31b] = 1u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
queue.context[0x31b] = 1u << 13u;
const auto firstSlice = AgcDriver::Graphics::DecodeState(queue);
Require(firstSlice.color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()), "a color view of several slices did not retain its first slice");
queue.context[0x207] = 1u << 18u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices");
queue.context[0x207] = 0;
const auto sliced = reinterpret_cast<std::uintptr_t>(sliceMemory.data());
queue.context[0x318] = static_cast<std::uint32_t>(sliced >> 8u);
queue.context[0x390] = static_cast<std::uint32_t>(sliced >> 40u);
@ -1355,13 +1551,35 @@ void DepthStencilTests() {
queue.context[0x31b] = 2u | (2u << 13u);
const auto volume = AgcDriver::Graphics::DecodeState(queue);
Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice");
Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered");
queue.context[0x31b] = 4u | (4u << 13u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the 3D surface");
queue.context[0x31b] = 2u | (3u << 13u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "several 3D slices");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface");
queue.context[0x31b] = 2u | (1u << 13u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
queue.context[0x31b] = 1u | (3u << 13u);
queue.context[0x207] = (1u << 18u) | (1u << 21u) | (1u << 24u);
const auto layered = AgcDriver::Graphics::DecodeState(queue);
Require(layered.color.depthSlice == 1u && layered.color.layers == 3u && layered.renderLayers == 3u && layered.layerExports == (1u << 18u), "a color view of three 3D depth slices did not render three layers from the vertex layer export");
{
auto pointSize = queue;
pointSize.context[0x1b3] = pointSize.context[0x1b4] = 2;
Require(AgcDriver::Graphics::DrawRejection(pointSize, false).empty(), "layered volume test state failed the draw precheck");
pointSize.context[0x207] |= 1u << 16u;
const auto rejection = AgcDriver::Graphics::DrawRejection(pointSize, false);
Require(rejection.empty(), "inactive point size rejected a layered volume draw: " + rejection);
const auto decoded = AgcDriver::Graphics::DecodeState(pointSize);
Require(decoded.color.depthSlice == layered.color.depthSlice && decoded.color.layers == layered.color.layers && decoded.renderLayers == layered.renderLayers && decoded.layerExports == layered.layerExports, "inactive point size changed layered volume routing");
Require(decoded.paClVsOutCntl == pointSize.context[0x207], "inactive point size discarded the layered export state");
}
queue.context[0x31b] = 1u | (8u << 13u);
Require(AgcDriver::Graphics::DecodeState(queue).renderLayers == 3u, "a color view past the 3D surface was not clamped to its last slice");
queue.context[0x207] = 0;
queue.context[0x31b] = 0;
queue.context[0x31c] |= 0x10000000;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets");
const auto volumeKeys = reinterpret_cast<std::uintptr_t>(volumeKeyMemory.data());
queue.context[0x325] = static_cast<std::uint32_t>(volumeKeys >> 8u);
queue.context[0x3a8] = static_cast<std::uint32_t>(volumeKeys >> 40u);
Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == volumeKeys, "a DCC 3D color target lost its keys");
}
void OneDimensionalColorTests() {
@ -1660,6 +1878,15 @@ void metadataPassTests() {
queue.context[0x0] = 0;
queue.context[0x200] = 0x70;
Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass");
auto bound = queue;
bound.context[0x010] = 0x3;
bound.context[0x011] = 0x1;
bound.context[0x200] = 0x72;
Require(DecodeColorMetadataPass(bound).has_value(), "an always-pass depth test without writes refused the pass");
bound.context[0x200] = 0x76;
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
bound.context[0x200] = 0x7a;
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
queue.context[0x91] = 0x40020;
expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target");
queue.context[0x91] = 0x40040;
@ -1825,6 +2052,16 @@ void cmaskTests() {
auto mipmapped = queue;
mipmapped.context[0x3b0] |= 1u << 28u;
expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array");
constexpr std::size_t blockBytes = 65536u;
static std::vector<std::byte> mipStorage(16u * blockBytes);
const auto mipBase = (reinterpret_cast<std::uintptr_t>(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes;
auto higherMip = queue;
higherMip.context[0x318] = static_cast<std::uint32_t>(mipBase >> 8u);
higherMip.context[0x390] = static_cast<std::uint32_t>(mipBase >> 40u);
higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u;
higherMip.context[0x31b] = 1u << 26u;
const auto higherTarget = DecodeColorBuffer(higherMip.context, 0);
Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK");
auto unaddressed = queue;
unaddressed.context[0x31f] = 0;
unaddressed.context[0x398] = 0;
@ -1896,8 +2133,8 @@ void cmaskTests() {
cmaskMemory.fill(0xff);
setTile(2, 0);
setTile(3, 3);
expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not a single-sample fast-clear code");
Require(texels(0x5a5a5a5au), "a refused pass changed the texels");
AgcDriver::Graphics::RunColorMetadataPass(context, *pass);
Require(tiles([](std::uint32_t tile) { return tile == 2; }) && cmaskIs(0xff), "a tile with a CMASK code other than 0 or 15 did not keep its texels");
queue.context[0x31c] |= 0x10000000;
const auto keysAddress = reinterpret_cast<std::uintptr_t>(dccKeys.data());
@ -2073,6 +2310,54 @@ void multisampleTests() {
state = DecodeState(queue);
Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard");
AgcDriver::Graphics::ValidateSampleLocations({}, state);
for (const auto log2 : {0u, 1u, 2u, 3u}) {
queue = log2 == 0 ? makeState() : multisampled();
queue.context[0x1b3] = 2u;
queue.context[0x1b4] = 2u;
queue.context[0x2f8] = (log2 << 20u) | log2 | 0x20000000u;
queue.context[0x31d] = (log2 << 12u) | (log2 << 15u);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
queue.context[0x2fe + pixel * 4u] = log2 == 3 ? 0xbd153fd1u : log2 == 2 ? 0x622ae6aeu : 0xcc44u;
queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u;
}
std::vector<AgcDriver::Graphics::RegisterRead> sampleReads;
AgcDriver::Graphics::RegisterReadLog() = &sampleReads;
const auto coveredCenter = DecodeState(queue);
const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false);
AgcDriver::Graphics::RegisterReadLog() = nullptr;
Require(coveredCenter.samples == (1u << log2) && !coveredCenter.customSampleLocations, "covered-centroid mode changed the sample count or locations without centroid inputs");
for (const auto read : sampleReads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a covered-centroid register");
Require(coveredRejection.empty(), "covered-centroid mode without centroid inputs was refused: " + coveredRejection);
for (const auto centroid : {4u, 64u, 68u}) {
queue.context[0x1b3] = 2u | centroid;
queue.context[0x1b4] = 2u | centroid;
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted active covered-centroid interpolation");
queue.context[0x1b4] = 2u;
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an unaddressed centroid input changed covered-centroid mode");
queue.context[0x1b3] = 2u;
queue.context[0x1b4] = 2u | centroid;
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a disabled centroid input changed covered-centroid mode");
}
queue.context[0x1b3] = 2u;
queue.context[0x1b4] = 2u;
for (const auto offset : {0x1b3u, 0x1b4u}) {
queue.context.erase(offset);
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted unspecified centroid inputs");
queue.context[offset] = 2u;
}
const auto withProgram = queue;
queue.shader[0x008] = 0;
queue.context[0x8e] = 0;
queue.context.erase(0x1b3);
queue.context.erase(0x1b4);
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "covered-centroid mode was refused with no pixel program");
queue = withProgram;
queue.context[0x2f8] |= 0x10u;
expectFailure([&] { DecodeState(queue); }, "coverage conversion or nonstandard multisampling");
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("coverage conversion or nonstandard multisampling") != std::string::npos, "covered-centroid handling admitted unrelated AA bits");
}
queue = multisampled();
queue.context[0x31d] = 0x1000;
expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples");
@ -2262,6 +2547,18 @@ void clipDistanceTests() {
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck");
}
void framebufferLayerTests() {
AgcDriver::Graphics::Context context{};
context.limits.maxFramebufferWidth = 64;
context.limits.maxFramebufferHeight = 64;
context.limits.maxFramebufferLayers = 3;
AgcDriver::Graphics::Framebuffer accepted(context, VK_NULL_HANDLE, {}, {64, 64}, 3);
for (const VkRenderPass pass : std::array<VkRenderPass, 2>{VK_NULL_HANDLE, reinterpret_cast<VkRenderPass>(1)}) {
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 0); }, "framebuffer layers exceed device limits");
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 4); }, "framebuffer layers exceed device limits");
}
}
void DepthClipTests() {
auto queue = makeState();
const auto direct = AgcDriver::Graphics::DecodeState(queue);
@ -2339,7 +2636,7 @@ void AlphaToCoverageTests() {
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage");
}
queue.context[0x2dc] = 0xaa01;
const auto exportFormat = queue.context.at(0x1c5);
const std::uint32_t exportFormat = queue.context.at(0x1c5);
for (const auto format : {4u, 5u, 6u, 9u}) {
queue.context[0x1c5] = (exportFormat & ~0xfu) | format;
Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha");
@ -2739,6 +3036,32 @@ AgcDriver::Graphics::Context mockContext() {
return context;
}
void addressSpaceTeardownTests() {
#ifdef _WIN32
_putenv_s("APS5_NO_LEASE_MIRROR", "1");
#else
setenv("APS5_NO_LEASE_MIRROR", "1", 1);
#endif
auto context = mockContext();
context.device = makeHandle<VkDevice>();
context.bufferDeviceAddress = true;
context.limits.maxStorageBufferRange = 1u << 27;
void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64);
const auto before = AgcDriver::Graphics::LeaseCounters().cacheDrops;
{
AgcDriver::Graphics::GuestBufferMemory memory(context);
memory.AcquireRegistered();
Require(memory.HoldsLease(), "address-based build holds no lease before device teardown");
memory.Upload(true);
memory.WriteBack();
Require(!memory.HoldsLease(), "completed address-based build kept its own lease");
}
AgcDriver::Graphics::ClearImageMirrors(context.device);
GuestHeap::GuestHeapFree_nid_postfix(block);
Require(AgcDriver::Graphics::LeaseCounters().cacheDrops == before, "device teardown left the cached address space pinning guest allocations");
Require(mock.live == 0, "address-space teardown test leaked Vulkan objects");
}
using Role = ShaderRecompiler::DescriptorRole;
using Kind = ShaderRecompiler::DescriptorKind;
@ -3372,6 +3695,7 @@ struct ModuleShape {
std::uint32_t floatControlsWidth = 0;
std::uint32_t clipDistanceLength = 0;
std::uint32_t cullDistanceLength = 0;
bool layerOutput = false;
};
void emit(std::vector<std::uint32_t>& out, spv::Op op, std::initializer_list<std::uint32_t> operands) {
@ -3410,6 +3734,14 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, shape.parameterLocation});
extraInterface.push_back(parameter);
}
if (shape.layerOutput) {
const auto pointer = id();
const auto layer = id();
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, uintType});
emit(declarations, spv::OpVariable, {pointer, layer, spv::StorageClassOutput});
emit(annotations, spv::OpDecorate, {layer, spv::DecorationBuiltIn, spv::BuiltInLayer});
extraInterface.push_back(layer);
}
if (shape.secondTarget) {
const auto target = id();
emit(declarations, spv::OpVariable, {outputPointer, target, spv::StorageClassOutput});
@ -3566,6 +3898,15 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry});
if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance});
if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance});
if (shape.layerOutput) {
emit(words, spv::OpCapability, {spv::CapabilityShaderViewportIndexLayerEXT});
const std::string extension = "SPV_EXT_shader_viewport_index_layer";
const auto count = (extension.size() + 4) / 4;
words.push_back((static_cast<std::uint32_t>(count + 1) << 16u) | spv::OpExtension);
const auto start = words.size();
words.resize(start + count, 0);
for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast<std::uint32_t>(static_cast<unsigned char>(extension[i])) << ((i % 4) * 8);
}
if (shape.barycentric) {
emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR});
const std::string extension = "SPV_KHR_fragment_shader_barycentric";
@ -4048,6 +4389,41 @@ void conservativeZExportTests() {
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode");
}
bool declaresEarlyFragmentTests(const ShaderRecompiler::RecompileRequest& request) {
const auto result = ShaderRecompiler::Recompile(request);
const auto& words = result.spirv.Words();
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) return true;
}
return false;
}
void earlyFragmentTestsTests() {
const std::array<std::pair<std::uint32_t, bool>, 9> cases{{
{0x0000u, false},
{0x0020u, false},
{0x0010u, true},
{0x0030u, true},
{0x0050u, false},
{0x0011u, false},
{0x1000u, true},
{0x1050u, true},
{0x1031u, true}
}};
const ShaderRecompiler::RequestSerializer serializer;
for (const auto& [shaderControl, expected] : cases) {
const auto request = depthExportRequest(shaderControl);
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
Require(declaresEarlyFragmentTests(request) == expected && declaresEarlyFragmentTests(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + (expected ? " did not declare" : " declared") + " early fragment tests");
}
for (const auto shaderControl : {0x0010u, 0x1000u}) {
auto request = depthExportRequest(shaderControl);
request.context.pixel->alphaToCoverage = true;
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests");
}
}
constexpr std::array<std::uint32_t, 5> OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u};
constexpr std::array<std::uint32_t, 12> OrderedBdaPixelCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf800180fu, 0x07070707u, 0xbf810000u};
@ -4251,6 +4627,7 @@ void stencilRefExportTests() {
Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl));
}
Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference");
Require(AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x1012u, 2u).context, IdentityExports).earlyZ, "DEPTH_BEFORE_SHADER with a stencil reference export did not get early tests");
const std::array<std::uint32_t, 2> stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT};
const std::array<std::uint32_t, 1> plain{spv::CapabilityShader};
const ShaderRecompiler::RequestSerializer serializer;
@ -4920,6 +5297,7 @@ void validationTests() {
vertex.vertexAttributes[0].components = 2;
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range");
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 64, "vertex range includes records before the first vertex");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch");
{
auto wider = attribute;
@ -4942,6 +5320,7 @@ void validationTests() {
Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed");
}
attribute.fetchIndex = 1;
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 224, "instance range includes records before the first instance");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 2) == 112, "a first instance past the records still spans its fetch");
@ -4959,6 +5338,7 @@ void validationTests() {
attribute.resource.fields[1] = 0;
attribute.resource.fields[2] = 16;
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value");
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value");
attribute.resource.fields[2] = 8;
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range");
@ -5034,6 +5414,14 @@ void validationTests() {
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 12");
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true);
}
{
ShaderRecompiler::RecompileResult vertex;
vertex.spirv = makeModule({.layerOutput = true});
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "VK_EXT_shader_viewport_index_layer");
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true);
}
for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) {
ShaderRecompiler::RecompileResult vertex;
vertex.spirv = makeModule({});
@ -5389,6 +5777,21 @@ int main(int argc, char** argv) {
std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n';
return passed ? 0 : 1;
}
if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") {
addressSpaceTeardownTests();
std::cout << "address-space device teardown releases cached guest allocations\n";
return 0;
}
{
AgcDriver::Graphics::PipelineCacheSynchronization synchronization;
auto pipelineUse = synchronization.LockForPipelineUse();
bool snapshotAcquired = false;
std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); });
snapshot.join();
Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation");
pipelineUse.unlock();
Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished");
}
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") {
descriptorLayoutFlagTests();
std::cout << "Descriptor layout native and captured binding flags passed\n";
@ -5399,6 +5802,11 @@ int main(int argc, char** argv) {
std::cerr << "descriptor pool reset failure did not terminate\n";
return 1;
}
if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") {
multisampleTests();
std::cout << "Covered-centroid and multisample state checks passed\n";
return 0;
}
{
const AgcDriver::Graphics::Context context{};
const AgcDriver::Graphics::State state{};
@ -5445,6 +5853,7 @@ int main(int argc, char** argv) {
unorm10_11_11TargetTests();
DepthBoundsBiasTests();
conservativeZExportTests();
earlyFragmentTestsTests();
orderedPixelShaderTests();
stencilRefExportTests();
aliasedBufferViewTests();
@ -5455,6 +5864,7 @@ int main(int argc, char** argv) {
DisabledColorTests();
OpaqueDestinationAlphaTests();
Color1555TargetTests();
BlendBypassTests();
CompactedExportTests();
ReversedComponentOrderTests();
ShadersWriteMemoryTests();
@ -5466,8 +5876,11 @@ int main(int argc, char** argv) {
multisampleTests();
MsaaEnableSingleSampleTests();
clipDistanceTests();
framebufferLayerTests();
UnwrittenSampleLocationTests();
ShaderStageTests();
NggVertexWorkgroupTests();
inactiveVertexPointSizeTests();
TuningFieldTests();
PixelInputLayoutTests();
UnexportedPixelInputTests();

View File

@ -0,0 +1,93 @@
#include "IntermediateRepresentation/IrBuilder.hpp"
#include "IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/DeadCodeEliminator.hpp"
#include <cstdio>
#include <memory>
#include <stdexcept>
#include <vector>
using namespace ShaderRecompiler;
int main() {
try {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& value = builder.Constant(42u);
IrSsaValues<NumScalarRegs> table;
const auto& readTable = table;
if (readTable[0] != nullptr || table.HasStorage()) throw std::runtime_error("an empty SSA table allocated storage during a read");
for (IrValue* entry : readTable) if (entry != nullptr) throw std::runtime_error("an empty SSA table contained a value");
if (table.HasStorage()) throw std::runtime_error("iterating an empty SSA table allocated storage");
table[3] = &value;
if (!table.HasStorage() || readTable[3] != &value || readTable[2] != nullptr) throw std::runtime_error("an SSA write lost zero initialization or its value");
table.fill(nullptr);
if (table.HasStorage() || readTable[3] != nullptr) throw std::runtime_error("clearing an SSA table retained construction storage");
const auto id = value.Id();
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&value}));
auto& identity = program.CreateValue(IrOpcode::Identity, IrType::U32);
identity.AddArgument(&value);
block.AppendInstruction(&identity);
DeadCodeEliminator{}.RemoveIdentities(program);
auto& first = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
auto& second = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
first.AddArgument(&second);
second.AddArgument(&first);
std::vector<IrValue*> pinned;
for (std::uint32_t number = 100u; number < 112u; ++number) pinned.push_back(&builder.Constant(number));
block.ssaScalarValues[0] = pinned[0];
block.ssaVectorValues[0] = pinned[1];
block.ssaThreadBitScalarValues[0] = pinned[2];
block.ssaScalarMaskTags[0] = pinned[3];
program.Metadata().dynamicReads.push_back(pinned[4]);
program.Metadata().blockInfo.emplace_back();
program.Metadata().blockInfo.back().condition = pinned[5];
program.Metadata().blockInfo.back().indirectTarget = pinned[6];
auto& resources = program.Resources();
resources.descriptorSources.emplace_back();
resources.descriptorSources.back().dwords[0] = pinned[7];
resources.srtReads.push_back({pinned[8], 0u});
resources.controlFlow.emplace_back();
resources.controlFlow.back().condition = pinned[9];
resources.uniformFill.values[0] = pinned[10];
auto external = std::make_unique<IrValue>(IrOpcode::Identity, IrType::U32, value.Id());
external->AddArgument(pinned[11]);
resources.valueStorage.push_back(std::move(external));
const auto removed = program.ReclaimUnusedValues();
if (removed != 3u) throw std::runtime_error("detached identity and cyclic garbage were not reclaimed");
if (value.Id() != id || value.ImmediateU32() != 42u || value.Uses().size() != 1u) throw std::runtime_error("a live instruction lost its value or retained an unused backlink");
for (std::size_t index = 0; index < pinned.size(); ++index) {
if (pinned[index]->ImmediateU32() != 100u + index) throw std::runtime_error("a metadata or resource-owned value was reclaimed");
}
if (program.ReclaimUnusedValues() != 0u) throw std::runtime_error("reclaiming an unchanged graph was not idempotent");
auto& unused = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
IrValue untracked(IrOpcode::Identity, IrType::U32, value.Id());
untracked.AddArgument(&unused);
bool rejected = false;
try {
static_cast<void>(program.ReclaimUnusedValues());
} catch (const std::runtime_error&) {
rejected = true;
}
if (!rejected) throw std::runtime_error("an untracked external owner was accepted");
untracked.ReplaceArgument(0, nullptr);
program.Metadata().dynamicReads.clear();
if (program.ReclaimUnusedValues() != 2u) throw std::runtime_error("reclamation did not recover after an external-owner rejection");
if (builder.Constant(999u).Id() <= pinned.back()->Id()) throw std::runtime_error("reclamation reused a value ID");
IrProgram moved = std::move(program);
if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner");
IrProgram assigned;
static_cast<void>(assigned.CreateValue(IrOpcode::IAdd32, IrType::U32));
assigned = std::move(moved);
if (value.Id() != id || value.ImmediateU32() != 42u || assigned.ReclaimUnusedValues() != 0u) throw std::runtime_error("move assignment invalidated a live allocation owner");
if (assigned.CreateValue(IrOpcode::IAdd32, IrType::U32).Id() <= pinned.back()->Id()) throw std::runtime_error("a transferred allocation owner reused a value ID");
std::puts("IR reclamation ownership tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -10,6 +10,7 @@
#include <cstring>
#include <iostream>
#include <new>
#include <span>
#include <string>
#include <vector>
@ -46,6 +47,13 @@ alignas(256) constexpr auto Wave32GeometryCode = [] {
return code;
}();
alignas(256) constexpr std::array<std::uint32_t, 25> FastLaunchCode{
0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010,
0x80020601, 0xb07c1003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302,
0xf800020f, 0x09080706, 0x7d840080, 0xbefe036a, 0x7e1402ff, 0x00200400, 0xf8000941, 0x0000000a,
0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 7> PixelCode{
0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000,
};
@ -132,6 +140,8 @@ struct MeshDraw {
bool restart = false;
std::uint32_t waveSize = 64u;
VkCullModeFlags cull = VK_CULL_MODE_NONE;
std::span<const std::uint32_t> code = {};
std::uint32_t stagesRegister = 0x20u;
};
bool ReadsNumWorkgroups(const std::vector<std::uint32_t>& words) {
@ -160,7 +170,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw);
std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8);
const auto& code = setup.waveSize == 32u ? Wave32GeometryCode : GeometryCode;
const std::span<const std::uint32_t> code = !setup.code.empty() ? setup.code : setup.waveSize == 32u ? std::span<const std::uint32_t>(Wave32GeometryCode) : std::span<const std::uint32_t>(GeometryCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> geometryMemory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(std::span(code))}}};
ShaderRecompiler::RecompileRequest geometry{
{ShaderStage::Mesh, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
@ -194,7 +204,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, setup.waveSize, 64, setup.mesh, std::nullopt};
state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
@ -215,6 +225,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u};
constexpr ShaderRecompiler::MeshConfiguration WideSubgroup{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u};
constexpr ShaderRecompiler::MeshConfiguration FastLaunchSubgroup{1u, 1u, 1u, 3u, 1u, 32u, 128u, 0u, 4u};
std::size_t FanPixel(float angle, float radius) {
const auto x = static_cast<std::uint32_t>((radius * std::cos(angle) + 1.0f) * 0.5f * Width);
@ -344,6 +355,24 @@ int main() {
CheckTriangles("non-indexed wave32 triangle list");
}
ClearPixels();
DrawMesh(device, {FastLaunchSubgroup, {0, Triangles, 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
CheckTriangles("fast-launch subgroups");
ClearPixels();
DrawMesh(device, {FastLaunchSubgroup, {0, Triangles / 2u, 0, 1, 0, false, Triangles / 4u}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) {
const bool drawn = triangle >= Triangles / 4u && triangle < Triangles / 4u + Triangles / 2u;
Require(PixelIs(CellPixel(triangle), drawn ? TriangleColor(triangle) : Background), "fast-launch subgroups from a start vertex: triangle " + std::to_string(triangle) + " pixel " + PixelText(CellPixel(triangle)));
}
bool fastLaunchIndexedRefused = false;
try {
DrawMesh(device, {FastLaunchSubgroup, {reinterpret_cast<std::uintptr_t>(Indices.data()), Triangles, 2, 1, 0, true}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
} catch (const std::runtime_error& error) {
fastLaunchIndexedRefused = std::string(error.what()).find("indexed fast-launch draws are unsupported") != std::string::npos;
if (!fastLaunchIndexedRefused) throw;
}
Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn");
constexpr std::size_t recordBlockBytes = 65536;
auto* record = static_cast<std::uint32_t*>(::operator new(recordBlockBytes, std::align_val_t{65536}));
{

View File

@ -2,6 +2,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libc/include/Shutdown.hpp"
@ -50,8 +51,10 @@ std::uint32_t low(const void* pointer) { return static_cast<std::uint32_t>(reint
std::uint32_t high(const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer) >> 32u); }
void execute(AgcDriver::QueueState& state, const std::vector<std::uint32_t>& packet) {
AgcDriver::DriverDetail::RegisterStateKey(state);
AgcDriver::Pm4::Validate(packet, 0);
AgcDriver::Pm4::Execute(packet, state);
check(AgcDriver::DriverDetail::RegisterStateKey(state) == AgcDriver::DriverDetail::RegisterStateKey(state, false, false), "PM4 changed registers without updating their fingerprint");
}
void testCatalog() {

View File

@ -27,9 +27,13 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "ResidentPresent.hpp"
#include "SampleLod_spv.h"
#include "BufferUpdate_spv.h"
#include "SampleArray_spv.h"
#include "Cover_vert_spv.h"
#include "Cover_frag_spv.h"
#include "VertexRange_vert_spv.h"
#include "VertexRangeProducer_frag_spv.h"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp"
#include <SDL_loadso.h>
#ifdef _WIN32
#ifndef NOMINMAX
@ -306,6 +310,240 @@ private:
using Kind = Recorder::ReadKind;
void copiedDirectOrderingTests(const Device& device, Recorder& recorder) {
auto context = device.GetContext();
context.nullDescriptors = device.NullDescriptors();
if (context.hostImportAlignment == 0 || !context.nullDescriptors || !AgcDriver::GuestMemory::WriteWatched()) {
std::cout << "Host imports, null descriptors or write watching unavailable: copied/direct ordering test skipped\n";
return;
}
recorder.Sync();
const auto bytes = static_cast<std::size_t>(std::max<VkDeviceSize>(131072, context.hostImportAlignment));
auto* block = static_cast<std::uint32_t*>(AllocateWatched(bytes, bytes));
Require(block != nullptr, "allocate copied/direct ordering memory");
const auto address = reinterpret_cast<std::uint64_t>(block);
std::vector<std::shared_ptr<ShaderResources>> writers;
context.copiedWriters = &writers;
struct Release {
const Context& context;
Recorder& recorder;
std::uint32_t* block;
std::size_t bytes;
std::vector<std::shared_ptr<ShaderResources>>& writers;
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout layout = VK_NULL_HANDLE;
VkShaderModule module = VK_NULL_HANDLE;
std::shared_ptr<ShaderResources> layoutOwner;
~Release() {
recorder.Sync();
writers.clear();
DrawCopiedWriters()->clear();
if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
if (layout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, layout, nullptr);
if (module) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
static_cast<void>(HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes));
recorder.Sync();
ReleaseWatched(block, bytes);
}
} release{context, recorder, block, bytes, writers};
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
std::memset(block, 0, bytes);
Require(HostImportFor(context, address, bytes) != nullptr, "import copied/direct ordering memory");
ShaderRecompiler::RecompileResult program;
ShaderRecompiler::DescriptorBinding binding;
binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
binding.descriptorSet = 0;
binding.binding = 0;
binding.count = 1;
binding.guestDescriptor = {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, 1024, 0x31000000u};
binding.bufferWritten = {true};
program.bindings.push_back(binding);
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
auto direct = std::make_shared<ShaderResources>(context, compute);
release.layoutOwner = direct;
Require(!direct->HasCopiedWrites(), "ordering consumer did not bind directly");
const auto setLayout = direct->Layout();
const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(std::uint32_t)};
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layoutInfo.setLayoutCount = 1;
layoutInfo.pSetLayouts = &setLayout;
layoutInfo.pushConstantRangeCount = 1;
layoutInfo.pPushConstantRanges = &push;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &release.layout), "ordering pipeline layout");
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = sizeof(BUFFER_UPDATE_SPV);
moduleInfo.pCode = BUFFER_UPDATE_SPV;
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &release.module), "ordering shader module");
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, release.module, "main", nullptr};
pipelineInfo.layout = release.layout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &release.pipeline), "ordering pipeline");
const auto update = [&](const std::shared_ptr<ShaderResources>& resources, std::uint32_t delta, std::uint32_t count = 1) {
const auto commands = recorder.Commands();
recorder.Keep(resources);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.pipeline);
resources->Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.layout);
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, release.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(delta), &delta);
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, count, 1, 1);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_READ_BIT);
resources->MarkGpuWrites(recorder);
};
for (int mode = 0; mode < 3; ++mode) {
*block = 10;
auto copiedContext = context;
copiedContext.hostImportAlignment = 0;
copiedContext.dmaBufImport = false;
auto copied = std::make_shared<ShaderResources>(copiedContext, compute);
Require(copied->HasCopiedWrites(), "ordering producer did not take the copied path");
auto& pending = mode == 2 ? *DrawCopiedWriters() : writers;
pending.push_back(copied);
update(copied, 1);
recorder.OnComplete([copied, &pending] {
copied->WriteBackBuffers();
std::erase(pending, copied);
});
Require(!recorder.Idle() && *block == 10, "copied producer was completed before the consumer test");
if (mode == 0) direct = std::make_shared<ShaderResources>(context, compute);
else Require(direct->Revalidate(std::span(&compute, 1)), "the imported template could not be revalidated");
update(direct, 2);
recorder.Sync();
Require(*block == 13, mode == 0 ? "fresh direct consumer ran before copied write-back" : "reused direct consumer ran before copied write-back");
}
*block = 20;
block[1024] = 30;
auto copiedContext = context;
copiedContext.hostImportAlignment = 0;
copiedContext.dmaBufImport = false;
auto copied = std::make_shared<ShaderResources>(copiedContext, compute);
writers.push_back(copied);
update(copied, 1);
recorder.OnComplete([copied, &writers] {
copied->WriteBackBuffers();
std::erase(writers, copied);
});
auto separateProgram = program;
separateProgram.bindings[0].guestDescriptor[0] += 4096;
const CompiledShader separateCompute{ShaderRecompiler::ShaderStage::Compute, &separateProgram, 0};
auto separate = std::make_shared<ShaderResources>(context, separateCompute);
Require(!writers.empty() && *block == 20, "a disjoint consumer waited for copied writes");
update(separate, 2);
recorder.Sync();
Require(*block == 21 && block[1024] == 32, "disjoint copied and direct writes changed each other");
const std::uint32_t label = 40;
static_cast<void>(recorder.Commands());
recorder.AfterCompletions(address, std::as_bytes(std::span(&label, 1)), 1000, 0, false);
Require(direct->Revalidate(std::span(&compute, 1)), "template reuse after a CPU completion label failed");
update(direct, 2);
recorder.Sync();
Require(*block == 42, "direct consumer preceded an overlapping CPU completion label");
update(direct, 1);
const auto submissions = recorder.Submissions();
Require(direct->Revalidate(std::span(&compute, 1)) && recorder.Submissions() == submissions, "ordinary direct work incurred a CPU write-back wait");
update(direct, 2);
recorder.Sync();
Require(*block == 45, "direct consumers lost GPU-ordered updates");
{
TextureDetiler detiler(context);
auto imageContext = context;
imageContext.detiler = &detiler;
TextureCache textureCache(imageContext);
imageContext.textureCache = &textureCache;
Buffer readback(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
struct Clear {
const Context& context;
Recorder& recorder;
~Clear() { recorder.Sync(); ClearCachedTextures(context.device); }
} clear{context, recorder};
ShaderRecompiler::RecompileResult imageProgram;
ShaderRecompiler::DescriptorBinding imageBinding;
imageBinding.kind = ShaderRecompiler::DescriptorKind::SampledImage;
imageBinding.role = ShaderRecompiler::DescriptorRole::GuestImages;
imageBinding.descriptorSet = 0;
imageBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding;
imageBinding.count = 1;
imageBinding.imageSamplers = {0};
imageBinding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
imageBinding.guestDescriptor = {static_cast<std::uint32_t>(address >> 8), static_cast<std::uint32_t>(address >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x91b00fac, 0, 0, 0, 0};
imageProgram.bindings.push_back(imageBinding);
const CompiledShader imageCompute{ShaderRecompiler::ShaderStage::Compute, &imageProgram, 0};
auto sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
recorder.Sync();
for (int reuse = 0; reuse < 2; ++reuse) {
auto producer = std::make_shared<ShaderResources>(copiedContext, compute);
writers.push_back(producer);
update(producer, 1);
recorder.OnComplete([producer, &writers] {
producer->WriteBackBuffers();
std::erase(writers, producer);
});
if (reuse == 0) sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "sampled image template did not revalidate");
const auto images = sampled->StorageImages();
Require(images.size() == 1, "sampled image did not use imported texture storage");
const auto commands = recorder.Commands();
recorder.Keep(sampled);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {1, 1, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, &copy);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
recorder.Sync();
std::uint32_t value = 0;
std::memcpy(&value, readback.Bytes().data(), sizeof(value));
Require(value == static_cast<std::uint32_t>(46 + reuse), "cached sampled image preceded a copied producer");
}
sampled.reset();
ClearCachedTextures(context.device);
const auto keys = address + 65536;
const auto keyBytes = DccKeyBytes(65536);
imageProgram.bindings[0].guestDescriptor[6] = (1u << 21) | (static_cast<std::uint32_t>((keys >> 8) & 0xffu) << 24);
imageProgram.bindings[0].guestDescriptor[7] = static_cast<std::uint32_t>(keys >> 16);
auto keyProgram = program;
keyProgram.bindings[0].guestDescriptor = {static_cast<std::uint32_t>(keys), static_cast<std::uint32_t>(keys >> 32u) & 0xffffu, static_cast<std::uint32_t>(keyBytes), 0x31000000u};
const CompiledShader keyCompute{ShaderRecompiler::ShaderStage::Compute, &keyProgram, 0};
for (int reuse = 0; reuse < 2; ++reuse) {
std::memset(reinterpret_cast<void*>(keys), 0, keyBytes);
AgcDriver::GuestMemory::MarkWritten(keys, keyBytes);
sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
recorder.Sync();
auto producer = std::make_shared<ShaderResources>(copiedContext, keyCompute);
writers.push_back(producer);
update(producer, 0x40404040, static_cast<std::uint32_t>(keyBytes / 4));
recorder.OnComplete([producer, &writers] {
producer->WriteBackBuffers();
std::erase(writers, producer);
});
if (reuse == 0) sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "metadata-only producer invalidated the sampled template");
const auto images = sampled->StorageImages();
Require(images.size() == 1, "metadata texture is not resident");
const auto commands = recorder.Commands();
recorder.Keep(sampled);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {1, 1, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, &copy);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
recorder.Sync();
std::uint32_t value = 0;
std::memcpy(&value, readback.Bytes().data(), sizeof(value));
Require(value == 0xff000000, "cached sampled image ignored copied compression-metadata writes");
}
}
std::cout << "Copied producers precede fresh and reused direct consumers\n";
}
void readTrackingTests(const Device& device, Recorder& recorder) {
Require(Recorder::ReadTracking(), "read tracking is off (APS5_COPY_READ_TRACKING=0 set?)");
Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x10000, 16), "a fresh recorder reports a read");
@ -1072,6 +1310,41 @@ void hostImportRetirementTests(const Device& device) {
Require(preservedEveryRecorder, "unlocked mapping changed recorder batches");
std::cout << "Host import retirement locking and GPU readback tests passed\n";
}
#ifdef _WIN32
void secondMappingImportTests(const Device& device) {
const auto& context = device.GetContext();
if (context.hostImportAlignment == 0) {
std::cout << "host imports unavailable: imports after a second mapping not tested\n";
return;
}
constexpr std::size_t bytes = 65536;
constexpr std::size_t page = 16384;
struct Release {
void* mapping = nullptr;
void* alias = nullptr;
std::int64_t physical = -1;
~Release() {
if (alias != nullptr) sceKernelMunmap(alias, page);
if (mapping != nullptr) sceKernelMunmap(mapping, bytes);
if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes);
}
} release;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, bytes, 0, &release.physical) == 0, "allocate the second mapping test backing");
Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, bytes) == 0, "map the second mapping test backing");
const auto address = reinterpret_cast<std::uint64_t>(release.mapping);
if (HostImportFor(context, address, bytes) == nullptr) {
std::cout << "host import of singly mapped direct memory refused: imports after a second mapping not tested\n";
return;
}
const auto first = HostImportSerial(context, address, bytes, true);
Require(first != 0 && HostImportSerial(context, address, bytes, true) == first, "singly mapped direct memory lost its import");
Require(sceKernelMapDirectMemory(&release.alias, page, 0x33, 0, release.physical, page) == 0, "map the first page a second time");
Require(HostImportFor(context, address, bytes) != nullptr, "direct memory was not imported again after a second mapping of its first page");
const auto second = HostImportSerial(context, address, bytes, true);
Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept");
HostImportFor(context, address, bytes);
}
#endif
void movedMetadataTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
@ -1996,6 +2269,19 @@ void drawSnapshotCapTests(const Device& device, Recorder& recorder) {
Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write");
std::memset(block, 0x11, bytes);
snapshotRecorder.Sync();
#ifdef _WIN32
Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "64") == 0, "cannot raise snapshot cap");
#else
Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "64", 1) == 0, "cannot raise snapshot cap");
#endif
const auto again = resources.PrepareDrawBindings(snapshotRecorder);
Require(again != nullptr && again->snapshots.size() == 1 && again->snapshots[0].address == smallAddress, "built resources read the draw snapshot cap again instead of keeping the one they were built with");
snapshotRecorder.Sync();
#ifdef _WIN32
Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot restore snapshot cap");
#else
Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot restore snapshot cap");
#endif
}
const auto firstAddress = address + 4096;
constexpr std::size_t firstBytes = 600 * 1024;
@ -4817,6 +5103,12 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
context.nullDescriptors = true;
TextureCache cache(context);
context.textureCache = &cache;
const auto clearTargets = [&] {
recorder.Submit();
device.WaitQueue();
recorder.Sync();
ClearMultisampleTargets(context.device);
};
const auto refused = [](const std::function<void()>& access, const std::string& expected, const std::string& what) {
std::string message;
try {
@ -4873,7 +5165,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}, "a texture read", "a texture read");
refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane");
AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels);
ClearMultisampleTargets(context.device);
clearTargets();
target.tileMode = ColorTileMode::RenderTarget;
{
std::lock_guard gpu(GpuMutex());
@ -4884,7 +5176,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}
refused([&] { AgcDriver::GuestMemory::Read(address + targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the tiled multisampled target");
AgcDriver::GuestMemory::Read(address + targetBytes, texels);
ClearMultisampleTargets(context.device);
clearTargets();
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) {
target.extent = {128, 64};
target.surfaceExtent = target.extent;
@ -4898,7 +5190,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}
refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 2x tile-height padding");
AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels);
ClearMultisampleTargets(context.device);
clearTargets();
}
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) {
target.extent = {64, 32};
@ -4913,9 +5205,48 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}
refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 8x tile-height padding");
AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels);
ClearMultisampleTargets(context.device);
clearTargets();
}
AgcDriver::GuestMemory::Read(address + 256u, texels);
target.extent = {side, side};
target.surfaceExtent = target.extent;
target.samples = 4;
VkImageView previous = VK_NULL_HANDLE;
for (const bool removeAllocation : {false, true}) {
VkImageView view = VK_NULL_HANDLE;
{
std::lock_guard gpu(GpuMutex());
view = MultisampleTargetView(context, target);
Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image");
recorder.Submit();
device.WaitQueue();
recorder.Sync();
}
{
GuestAllocations::Mutation mutation;
mutation.Protect(block, bytes, true, true, true, [] {});
}
refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after protection changed");
bool failed = false;
try {
GuestAllocations::Mutation mutation;
mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("unmap failed"); });
} catch (const std::runtime_error&) {
failed = true;
}
Require(failed, "the failed host unmap did not throw");
refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after a failed unmap");
{
GuestAllocations::Mutation mutation;
if (removeAllocation) mutation.Remove(block);
else mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {});
mutation.Add(block, bytes, true, true, true);
}
RequireNoMultisampleTarget(address, bytes, "a texture read");
AgcDriver::GuestMemory::Read(address + 256u, texels);
previous = view;
}
clearTargets();
std::cout << "Multisampled target guest access tests passed\n";
}
@ -5284,6 +5615,165 @@ void atomicViewTests(const Device& device, Recorder& recorder) {
}
}
bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool imported) {
if (!device.Graphics() || (imported && device.GetContext().hostImportAlignment == 0)) return false;
constexpr std::size_t unit = 65536;
constexpr std::size_t bytes = unit * 3;
void* watched = AllocateWatched(bytes, unit);
std::vector<std::byte> fallback(watched == nullptr ? bytes + unit : 0);
auto* block = watched != nullptr ? static_cast<std::byte*>(watched) : reinterpret_cast<std::byte*>((reinterpret_cast<std::uintptr_t>(fallback.data()) + unit - 1) & ~(unit - 1));
struct Release {
void* watched;
~Release() { if (watched != nullptr) ReleaseWatched(watched, bytes); }
} release{watched};
std::fill_n(block, bytes, std::byte{0});
const auto address = reinterpret_cast<std::uint64_t>(block);
auto context = device.GetContext();
context.recorder = &recorder;
if (!imported) {
context.hostImportAlignment = 0;
context.dmaBufImport = false;
}
TextureDetiler detiler(context);
context.detiler = &detiler;
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
struct Cleanup {
const Context& context;
Recorder& recorder;
void* block;
~Cleanup() {
recorder.Sync();
WaitForOptimizedPipelines(context.device);
SharedResourceCache().Clear();
ClearCachedTextures(context.device);
recorder.Sync();
ClearCachedPipelines(context.device);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes);
}
} cleanup{context, recorder, block};
if (imported && HostImportFor(context, address, bytes) == nullptr) return false;
const auto shader = [](auto& words, std::uint64_t variant) {
ShaderRecompiler::RecompileResult program;
program.spirv = std::vector<std::uint32_t>(std::begin(words), std::end(words));
program.variantId = variant;
return program;
};
auto producerVertex = shader(COVER_VERT_SPV, 101);
auto producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102);
auto vertex = shader(VERTEXRANGE_VERT_SPV, 103);
auto pixel = shader(COVER_FRAG_SPV, 104);
const std::array producerShaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &producerVertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &producerPixel, 0}};
const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}};
const auto stateFor = [](std::uint64_t base, VkFormat format, std::uint32_t elementBytes) {
State state{};
state.stages.path = ShaderPath::Vertex;
state.color = {base, {64, 64}, format, unit, 0xe4u, ColorTileMode::RenderTarget, elementBytes};
state.colors = {state.color};
state.hasColorTarget = true;
state.renderExtent = {64, 64};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, 64, 64, -64, 0, 1};
state.scissor = {{0, 0}, {64, 64}};
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
return state;
};
const auto producer = stateFor(address + unit, VK_FORMAT_R32G32B32A32_SFLOAT, 16);
const auto consumer = stateFor(address, VK_FORMAT_R8G8B8A8_UNORM, 4);
const AgcDriver::Pm4::DrawParameters triangle{0, 3, 0, 1, 0, false};
const auto produce = [&] {
Draw(context, producer, triangle, producerShaders);
WaitForOptimizedPipelines(context.device);
const std::array range{std::pair{address + unit, address + 2 * unit}};
Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending");
};
const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) {
return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), 77u << 12u}}, fetchIndex};
};
const auto check = [&](std::uint32_t edge) {
std::vector<std::byte> texels(64 * 64 * 4);
ReadColorTarget(consumer.color, texels);
for (std::size_t y = 0; y < 64; ++y) {
for (std::size_t x = 0; x < 64; ++x) {
const auto expected = x < edge ? std::byte{0} : std::byte{255};
for (std::size_t channel = 0; channel < 4; ++channel) Require(texels[(y * 64 + x) * 4 + channel] == expected, "the draw did not consume the GPU-produced vertex offset");
}
}
};
const auto reject = [&](auto action, const char* expected) {
try { action(); }
catch (const std::runtime_error& error) {
Require(std::string(error.what()).find(expected) != std::string::npos, error.what());
return;
}
throw std::runtime_error("an actual render-target alias was accepted");
};
for (const std::uint32_t fetchIndex : {0u, 1u}) {
producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102);
vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)};
auto draw = triangle;
if (fetchIndex == 0) draw.firstVertex = unit / 16;
else draw.firstInstance = unit / 16;
produce();
std::shared_ptr<const AgcDriver::DrawRecipe> recipe;
Draw(context, consumer, draw, shaders, {}, &recipe);
WaitForOptimizedPipelines(context.device);
const auto read = recorder.DescribePendingRead(address + unit, 16);
Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput), "the vertex regression used the wrong copied/imported path");
check(16);
Require(recipe != nullptr, "the range regression did not create a draw recipe");
std::vector<std::byte> cleared(unit);
AgcDriver::GuestMemory::Write(address, cleared);
for (std::size_t offset = 5; offset < producerPixel.spirv.size();) {
const auto count = producerPixel.spirv[offset] >> 16u;
Require(count != 0, "invalid producer SPIR-V");
if ((producerPixel.spirv[offset] & 0xffffu) == 43u && count == 4 && producerPixel.spirv[offset + 3] == 0x3f000000u) producerPixel.spirv[offset + 3] = 0x3f400000u;
offset += count;
}
producerPixel.variantId = 105;
produce();
const auto hit = DrawWithRecipe(context, consumer, draw, shaders, {}, *recipe);
WaitForOptimizedPipelines(context.device);
Require(hit.recorded, "the vertex range regression did not reuse its draw recipe");
check(24);
vertex.vertexAttributes = {attributeFor(address + unit, 16, fetchIndex)};
Draw(context, consumer, triangle, shaders);
WaitForOptimizedPipelines(context.device);
check(24);
vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)};
auto indirect = draw;
indirect.indirect.emplace();
reject([&] { Draw(context, consumer, indirect, shaders); }, "vertex buffer aliases the render target");
if (fetchIndex == 0) {
const std::array<std::uint16_t, 3> indices{0, 1, 2};
AgcDriver::GuestMemory::Write(address + 2 * unit, std::as_bytes(std::span(indices)));
auto indexed = draw;
indexed.indexed = true;
indexed.indexSize = 2;
indexed.indexAddress = address + 2 * unit;
produce();
Draw(context, consumer, indexed, shaders);
WaitForOptimizedPipelines(context.device);
check(24);
indexed.indexAddress = address;
reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target");
}
reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target");
vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)};
reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target");
}
std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n";
return true;
}
void pipelineCacheTests(const Device& device, bool benchmark) {
std::thread establishThreadedProcess([] {});
establishThreadedProcess.join();
@ -5716,6 +6206,148 @@ void pipelineLibraryTests(const Device& device) {
std::cout << "Pipeline library reuse tests passed\n";
}
#ifdef _WIN32
namespace StorageCpuSnapshotTest {
constexpr std::uint32_t Side = 256;
constexpr std::uint32_t Format32UInt = 20;
constexpr std::uint32_t TileR64KBX = 0x1b;
constexpr std::uint32_t Type2D = 9;
constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u;
constexpr std::size_t BlockBytes = 12 * SurfaceBytes;
class Block {
public:
explicit Block(const Context& context, bool watched = false) : context(context), watched(watched) {
if (watched) {
memory = GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536);
const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr);
Require(section != nullptr, "cannot allocate the shared texture section");
try { GuestArena::GuestArenaMap_nid_postfix(memory, BlockBytes, section, 0, PAGE_READWRITE); }
catch (...) { CloseHandle(section); throw; }
CloseHandle(section);
} else memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
Require(memory != nullptr, "cannot allocate the texture block");
std::memset(memory, 0x5a, BlockBytes);
GuestAllocations::Mutation().Add(memory, BlockBytes, true, true, true);
}
~Block() {
ClearCachedTextures(context.device);
GuestAllocations::Mutation().Remove(memory);
HostImportFor(context, Surface(0), BlockBytes);
if (watched) {
GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes);
} else VirtualFree(memory, 0, MEM_RELEASE);
}
std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast<std::uint64_t>(memory) + surface * SurfaceBytes; }
private:
const Context& context;
bool watched;
void* memory = nullptr;
};
std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t side = Side) {
return {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u),
((side - 1u) >> 2u) | ((side - 1u) << 14u),
0xfacu | (TileR64KBX << 20u) | (Type2D << 28u),
0u,
0u,
0u,
0u,
};
}
GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) {
const auto words = Descriptor(address, side);
return DecodeTextureResource(words);
}
void Run(Context context) {
TextureDetiler detiler(context);
context.detiler = &detiler;
for (const bool watched : {true, false}) {
Block block(context, watched);
const auto address = block.Surface(0);
Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "snapshot fixture has the wrong write tracking");
Require(HostImportFor(context, address, SurfaceBytes) != nullptr, "snapshot fixture requires a host import");
Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "host import changed the fixture's write tracking");
auto image = StorageTexture::Create(context, *context.detiler, Resource(address), 0);
const auto expectedBytes = watched ? 0u : SurfaceBytes;
Require(image->OriginalSnapshotBytes() == expectedBytes, "watched GPU-direct upload retained an unnecessary CPU surface snapshot");
const auto readPixel = [&] {
Buffer result(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
CommandBatch batch(context);
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {1, 1, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, result.Handle(), 1, &copy);
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
batch.SubmitAndWait();
result.Invalidate();
std::uint32_t pixel = 0;
std::memcpy(&pixel, result.Bytes().data(), sizeof(pixel));
return pixel;
};
Require(readPixel() == 0x5a5a5a5au, "snapshot upload returned incorrect GPU texels");
std::memset(reinterpret_cast<void*>(address), 0x6b, SurfaceBytes);
image->Refresh();
Require(readPixel() == 0x6b6b6b6bu, "snapshot refresh missed a CPU write");
Require(image->OriginalSnapshotBytes() == expectedBytes, "watched refresh allocated a CPU surface snapshot");
{
CommandBatch batch(context);
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
VkClearColorValue clear{};
clear.uint32[0] = 0x7c7c7c7cu;
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
batch.SubmitAndWait();
}
image->MarkDirty();
image->WriteBack();
std::uint32_t pixel = 0;
AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1)));
Require(pixel == 0x7c7c7c7cu, "snapshot write-back lost GPU texels");
Require(image->OriginalSnapshotBytes() == expectedBytes, "GPU-direct write-back allocated a CPU surface snapshot");
if (watched) {
AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes);
Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "snapshot fixture is still write-watched");
std::memset(reinterpret_cast<void*>(address), 0x8d, SurfaceBytes);
image->Refresh();
Require(image->OriginalSnapshotBytes() == SurfaceBytes, "unwatched refresh did not allocate its comparison snapshot");
Require(readPixel() == 0x8d8d8d8du, "unwatched refresh returned incorrect GPU texels");
}
image.reset();
Context cpu = context;
cpu.hostImportAlignment = 0;
image = StorageTexture::Create(cpu, *context.detiler, Resource(address), 0);
Require(image->OriginalSnapshotBytes() == SurfaceBytes, "CPU upload has no guest-byte snapshot");
const auto expectedPixel = watched ? 0x8d8d8d8du : 0x7c7c7c7cu;
Require(readPixel() == expectedPixel, "CPU upload returned incorrect GPU texels");
{
CommandBatch batch(context);
VkClearColorValue clear{};
clear.uint32[0] = 0x9e9e9e9eu;
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
batch.SubmitAndWait();
}
image->MarkDirty();
image->WriteBack();
AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1)));
Require(pixel == 0x9e9e9e9eu, "CPU write-back lost guest texels");
std::printf("%s snapshot fixture: direct %zu bytes, CPU fallback %zu bytes\n", watched ? "watched" : "unwatched", expectedBytes, image->OriginalSnapshotBytes());
}
}
}
#endif
int main(int argc, char** argv) {
try {
std::unique_ptr<Device> created;
@ -5750,9 +6382,25 @@ int main(int argc, char** argv) {
return 0;
}
std::unique_lock gpu(GpuMutex());
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--storage-cpu-snapshot-only") {
if (device.GetContext().hostImportAlignment == 0) {
Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "storage snapshot test requires host imports");
std::cout << "skipped, host imports unavailable\n";
return 77;
}
StorageCpuSnapshotTest::Run(device.GetContext());
std::cout << "Storage CPU snapshot tests passed\n";
return 0;
}
#endif
std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n';
Recorder recorder(device.GetContext());
recorder.Activate();
if (argc == 2 && std::string_view(argv[1]) == "--copied-direct-only") {
copiedDirectOrderingTests(device, recorder);
return 0;
}
if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) {
storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only");
return 0;
@ -5832,6 +6480,16 @@ int main(int argc, char** argv) {
return 0;
}
if (argc == 2 && (std::string_view(argv[1]) == "--vertex-target-range-copied" || std::string_view(argv[1]) == "--vertex-target-range-imported")) {
return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77;
}
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--host-import-second-mapping-only") {
secondMappingImportTests(device);
return 0;
}
#endif
readTrackingTests(device, recorder);
writeSettledTests(device, recorder);
completionCountTests(device, recorder);
@ -5862,6 +6520,9 @@ int main(int argc, char** argv) {
storeRunTests(device, recorder);
queuedWriteGroupTests();
remappedImportTests(device);
#ifdef _WIN32
secondMappingImportTests(device);
#endif
movedMetadataTests(device, recorder);
viewPastLastMipTests(device, recorder);
keysFillTests(device, recorder);
@ -5891,6 +6552,7 @@ int main(int argc, char** argv) {
cmaskPassTests(device, recorder);
pendingKeyStoreTests(device, recorder);
sampleDumpTests(device, recorder);
copiedDirectOrderingTests(device, recorder);
gpu.unlock();
hostImportUnmapTests(device, recorder);
drawSnapshotPatchTests(device);

View File

@ -0,0 +1,140 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <random>
#include <stdexcept>
#include <type_traits>
using namespace AgcDriver;
using namespace AgcDriver::DriverDetail;
namespace {
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWords = false) {
std::uint64_t key = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
key ^= value;
key *= 0x100000001b3ull;
};
const auto userEnd = [&](std::uint32_t base) {
if (allUserWords) return base + 32u;
const auto resources = queue.shader.find(base - 1);
if (resources == queue.shader.end()) return base;
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
return base + std::min(count, 32u);
};
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
const auto unread = [&](std::uint32_t offset) {
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
};
for (const auto& range : Graphics::DrawKeyRegisters) {
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
mix((static_cast<std::uint64_t>(range.bank) << 32u) | range.first);
const auto end = range.first + range.count;
const bool shader = range.bank == Graphics::RegisterBank::Shader;
for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) {
if (shader && unread(it->first)) continue;
mix(it->first);
mix(it->second);
}
}
return key;
}
void testFingerprints() {
static_assert(!std::is_convertible_v<Registers::Reference, std::uint32_t&>);
std::mt19937 random(42);
QueueState queue;
auto reference = baselineRegisterStateKey(queue);
auto tracked = RegisterStateKey(queue);
const auto compare = [&] {
const auto expected = baselineRegisterStateKey(queue);
const auto actual = RegisterStateKey(queue);
check(actual == RegisterStateKey(queue, false, false), "incremental fingerprint missed a mutation");
check(RegisterStateKey(queue, true) == RegisterStateKey(queue, true, false), "all-user fingerprint missed a mutation");
check((expected == reference) == (actual == tracked), "exact key changed its dependency set");
reference = expected;
tracked = actual;
};
for (const auto& range : Graphics::DrawKeyRegisters) {
auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
for (auto offset = range.first; offset < range.first + range.count; ++offset) {
bank.erase(offset); compare();
bank.emplace(offset, 0); compare();
bank.insert_or_assign(offset, 0); compare();
bank[offset] = random(); compare();
bank.at(offset) ^= 1; compare();
}
}
for (std::uint32_t i = 0; i < 30000; ++i) {
auto& bank = i % 3 == 0 ? queue.context : i % 3 == 1 ? queue.shader : queue.userConfig;
const auto offset = random() % 0x500;
switch (i % 11) {
case 0: bank.erase(offset); break;
case 1: bank.emplace(offset, random()); break;
case 2: bank[offset] |= random(); break;
case 3: bank[offset] &= random(); break;
case 6: bank[offset] += random(); break;
case 4: {
auto retained = bank[offset];
bank.insert_or_assign(0x1000, random());
compare();
retained = random();
break;
}
case 5: {
auto copy = bank;
copy.insert_or_assign(offset, random());
compare();
bank = copy;
break;
}
default: bank.insert_or_assign(offset, random()); break;
}
compare();
}
queue.shader[0x8b] = 0; compare();
queue.shader[0x8c] = random(); compare();
queue.shader[0x8b] = 2; compare();
queue.shader[0x8b] = 1u << 27u; compare();
queue.shader.erase(0x8b); compare();
queue.savedContext = queue.context;
queue.ClearContext(); compare();
queue.context = *queue.savedContext; compare();
auto moved = std::move(queue.context);
check(queue.context.empty(), "moved register bank retained its population");
queue.context.emplace(0x200, 1); compare();
queue.context = std::move(moved); compare();
queue.context = queue.context; compare();
queue.context.clear(); compare();
queue.context.emplace(0x200, 0); compare();
queue = QueueState{}; compare();
Registers registers{{0, 7}, {64, 9}};
auto low = std::make_shared<const Registers::Mask>(std::vector<std::uint64_t>{1});
auto high = std::make_shared<const Registers::Mask>(std::vector<std::uint64_t>{0, 1});
const auto lowHash = registers.Fingerprint(low);
check(registers.Fingerprint(high) != lowHash && registers.Fingerprint(low) == lowHash, "projection switch reused another mask's hash");
registers[64] = 8;
check(registers.Fingerprint(low) == lowHash && registers.Fingerprint(high) == registers.RecomputeFingerprint(*high), "projection switch lost a masked write");
}
}
int main() {
try {
testFingerprints();
std::puts("Register fingerprint dependencies and mutations passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -7,6 +7,9 @@
#include "Optimization/DescriptorBindingBuilder.hpp"
#include "ShaderCacheDirectory.hpp"
#include "ControlFlow/RequestSerializer.hpp"
#include "Optimization/RequestMemoryView.hpp"
#include "Optimization/SrtWalker.hpp"
#include "IntermediateRepresentation/IrBlock.hpp"
#include <algorithm>
#include <array>
#include <chrono>
@ -18,7 +21,10 @@
#include <future>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <random>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
@ -247,6 +253,7 @@ CompiledVariant sampleVariant() {
info.info.usesDma = true;
info.info.usesFaultBuffer = true;
info.info.rasterPrimitive = true;
info.info.layerExported = true;
info.bindings.pushDataStartDword = 2;
info.bindings.memoryOffsetDword = 1;
info.bindings.memoryOffsetCount = 5;
@ -415,6 +422,7 @@ void verifyKeySensitivity() {
changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; });
changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; });
changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; });
changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; });
changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; });
changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; });
changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; });
@ -1081,6 +1089,743 @@ void verifySpecializationLiveness() {
require(SpecializeSpirv(specialized) == specialized, "dead computation removal is not stable");
}
constexpr std::uint64_t PlanSrtAddress = 0x7f0000010000ull;
constexpr std::uint64_t PlanTableAddress = 0x7f0000020000ull;
constexpr std::uint64_t PlanMaterialAddress = 0x7f0000030000ull;
constexpr std::uint64_t PlanPayloadAddress = 0x7f0000040000ull;
constexpr std::uint64_t PlanPointerAddress = 0x7f0000048000ull;
constexpr std::uint64_t PlanOutputAddress = 0x7f0000050000ull;
constexpr std::uint64_t PlanTextureAddress = 0x7f0000060000ull;
constexpr std::uint64_t PlanUnmappedAddress = 0x7f00000f0000ull;
struct PlanFixture {
std::string name;
ShaderStage stage = ShaderStage::Compute;
std::uint64_t codeAddress = 0;
std::uint32_t waveSize = 64;
std::vector<std::uint32_t> code;
std::vector<std::uint32_t> userData;
std::vector<std::pair<std::uint64_t, std::vector<std::uint32_t>>> regions;
std::optional<ShaderPixelStageInfo> pixel;
std::vector<MemoryRegion> memory;
RecompileRequest Request(bool useCache) {
static constexpr std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilitySampledImageArrayDynamicIndexing, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance};
static constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
memory.clear();
for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))});
RecompileRequest request{};
request.shader = {stage, codeAddress, code, 0, {}};
request.context.waveSize = waveSize;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
if (stage == ShaderStage::Compute) request.context.compute = ShaderComputeStageInfo{{waveSize, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.context.pixel = pixel;
request.context.memory = memory;
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 32;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.bdaAbiVersion = 1u;
request.layout.pushConstantSizeBytes = 128;
request.useCache = useCache;
return request;
}
};
std::vector<std::uint32_t> imageWords(std::uint64_t base) {
return {static_cast<std::uint32_t>(base >> 8u), static_cast<std::uint32_t>((base >> 40u) & 0xffu) | (56u << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
}
std::vector<std::uint32_t> bufferWords(std::uint64_t base, std::uint32_t stride, std::uint32_t records, std::uint32_t format = 0xfacu) {
return {static_cast<std::uint32_t>(base), static_cast<std::uint32_t>((base >> 32u) & 0xffffu) | (stride << 16u), records, format};
}
void place(std::vector<std::uint32_t>& words, std::size_t offset, const std::vector<std::uint32_t>& values) {
std::copy(values.begin(), values.end(), words.begin() + static_cast<std::ptrdiff_t>(offset));
}
std::vector<PlanFixture> planFixtures() {
std::vector<PlanFixture> fixtures;
const std::vector<std::uint32_t> srtPointer{static_cast<std::uint32_t>(PlanSrtAddress), static_cast<std::uint32_t>(PlanSrtAddress >> 32u)};
const auto add = [&](std::string name, std::uint32_t waveSize, std::vector<std::uint32_t> code, std::vector<std::uint32_t> userData, std::vector<std::pair<std::uint64_t, std::vector<std::uint32_t>>> regions) {
PlanFixture fixture;
fixture.name = std::move(name);
fixture.codeAddress = 0x100000u + static_cast<std::uint64_t>(fixtures.size()) * 0x1000u;
fixture.waveSize = waveSize;
fixture.code = std::move(code);
fixture.userData = std::move(userData);
fixture.regions = std::move(regions);
fixtures.push_back(std::move(fixture));
};
add("user data buffer", 64, {0xe0700000u, 0x80000000u, 0xbf810000u}, bufferWords(PlanOutputAddress, 0u, 0x40u, 0x00027facu), {});
std::vector<std::uint32_t> guarded(32, 0u);
place(guarded, 0, imageWords(PlanTextureAddress));
place(guarded, 16, bufferWords(PlanOutputAddress, 0u, 16u));
guarded[20] = 1u;
add("guarded image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}});
add("guarded image stores", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}});
std::vector<std::uint32_t> heap(32, 0u);
place(heap, 0, imageWords(PlanTextureAddress));
place(heap, 8, imageWords(PlanTextureAddress + 0x100u));
place(heap, 24, imageWords(PlanTextureAddress));
const std::vector<std::uint32_t> materials{0u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 3u, 0u, 0u};
std::vector<std::uint32_t> tables(16, 0u);
place(tables, 0, bufferWords(PlanTableAddress, 32u, 4u));
place(tables, 8, bufferWords(PlanMaterialAddress, 16u, 3u));
place(tables, 12, bufferWords(PlanOutputAddress, 0u, 16u));
add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
std::vector<std::uint32_t> phis(32, 0u);
place(phis, 0, imageWords(PlanTextureAddress));
place(phis, 12, {0x49u, 0u, 0x00500000u, 0u});
place(phis, 16, bufferWords(PlanOutputAddress, 0u, 16u));
phis[20] = 1u;
place(phis, 24, imageWords(PlanTextureAddress + 0x100u));
add("descriptor phi sampler", 32, {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
add("descriptor phi image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
add("descriptor phi loop", 32, {0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
add("descriptor phi in sequence", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
std::vector<std::uint32_t> root(16, 0u);
place(root, 0, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu));
root[4] = 1u;
root[6] = static_cast<std::uint32_t>(PlanPayloadAddress);
root[7] = static_cast<std::uint32_t>(PlanPayloadAddress >> 32u);
const std::vector<std::uint32_t> payload{0x3f800000u};
add("guarded nested read", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, root}, {PlanPayloadAddress, payload}});
auto chained = root;
chained[6] = static_cast<std::uint32_t>(PlanPointerAddress);
chained[7] = static_cast<std::uint32_t>(PlanPointerAddress >> 32u);
add("guarded pointer chain", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPayloadAddress, payload}, {PlanPointerAddress, {static_cast<std::uint32_t>(PlanPayloadAddress), static_cast<std::uint32_t>(PlanPayloadAddress >> 32u)}}});
add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}});
add("descriptor invariant phi", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
PlanFixture fragment;
fragment.name = "fragment parameters";
fragment.stage = ShaderStage::Fragment;
fragment.codeAddress = 0x100000u + static_cast<std::uint64_t>(fixtures.size()) * 0x1000u;
fragment.code = {0xc8100000u, 0xc8110001u, 0xc8140402u, 0xc8150403u, 0xf800180fu, 0x05040504u, 0xbf810000u};
ShaderPixelStageInfo pixel{};
pixel.interpolatorCount = 2u;
pixel.interpolatorSettings[1] = 1u;
pixel.wave32 = true;
pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter);
pixel.hasPerspectiveCenterVgpr = true;
pixel.noPerspective = true;
pixel.targetOutputMode[0] = 9u;
pixel.targetExportMapping[0] = 0xe4u;
fragment.pixel = pixel;
fixtures.push_back(std::move(fragment));
return fixtures;
}
IrResourcePlan handBuiltPlan() {
IrResourcePlan plan;
std::uint32_t ids = 0;
const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& {
plan.valueStorage.push_back(std::make_unique<IrValue>(opcode, type, ids++));
return *plan.valueStorage.back();
};
const auto constant = [&](std::uint32_t value) -> IrValue& {
auto& immediate = make(IrOpcode::Void, IrType::U32);
immediate.SetImmediateU32(value);
return immediate;
};
auto& resource = make(IrOpcode::GetSrtResource, IrType::SrtResource);
const auto readConst = [&](std::uint32_t slot) -> IrValue& {
auto& read = make(IrOpcode::ReadConst, IrType::U32);
read.AddArgument(&resource);
read.AddArgument(&constant(slot));
return read;
};
const auto userData = [&](std::uint32_t index) -> IrValue& {
auto& reg = make(IrOpcode::Void, IrType::ScalarReg);
reg.SetRegister({RegisterBank::Scalar, index});
auto& read = make(IrOpcode::GetUserData, IrType::U32);
read.AddArgument(&reg);
return read;
};
auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64);
handle.AddArgument(&userData(0));
handle.AddArgument(&userData(1));
auto& pointer = make(IrOpcode::LoadAddressU32, IrType::U32);
pointer.AddArgument(&handle);
pointer.AddArgument(&constant(8));
plan.blockStorage.push_back(std::make_unique<IrBlock>(3u));
plan.blockStorage.push_back(std::make_unique<IrBlock>(7u));
auto& phi = make(IrOpcode::Phi, IrType::U32);
phi.AddPhiOperand(plan.blockStorage[0].get(), &readConst(0));
phi.AddPhiOperand(plan.blockStorage[1].get(), &constant(0));
auto& condition = make(IrOpcode::Void, IrType::Bool);
condition.SetImmediateU64(1u);
condition.SetFlags(std::uint64_t{0x1234u});
plan.stage = IrShaderStage::Compute;
plan.shaderHash = 0x123456789abcull;
plan.userDataBase = 2u;
plan.userDataCount = 4u;
plan.srgbDecodeFormats = 5u;
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.resource = 1u;
memory.dmask = 0xfu;
memory.imageDimension = RdnaImageDimension::Dim2D;
memory.gpuDescriptor = true;
memory.flushDenormals = true;
MemoryInfo every;
every.kind = ResourceKind::Buffer;
every.resource = 2u;
every.sampler = 3u;
every.offset = 0x14u;
every.secondaryOffset = 0x28u;
every.dmask = 0x5u;
every.dataDwords = 3u;
every.dataBits = 16u;
every.componentIndex = 1u;
every.componentCount = 2u;
every.dataFormat = 10u;
every.numberFormat = 7u;
every.imageSampleFlags = 0x21u;
every.imageDimension = RdnaImageDimension::Dim3D;
every.imageAddressComponents = 3u;
every.addressIsFull = true;
every.dataSigned = true;
every.typed = true;
every.formatted = true;
every.d16 = true;
every.imageHasMip = true;
every.imageR128 = true;
every.imagePacked = true;
every.imageByElements = 2u;
every.idxen = true;
every.offen = true;
every.planningOnly = true;
every.coherent = true;
every.gpuDescriptor = true;
every.flushDenormals = true;
plan.memoryInfo = {memory, every, MemoryInfo{}};
DescriptorSource buffer;
buffer.dwordCount = 4u;
buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer};
DescriptorSource table;
table.dwordCount = 8u;
table.dwords[0] = &readConst(3);
table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true};
DescriptorSource heap;
heap.dwordCount = 4u;
heap.dwords[1] = &readConst(6);
DescriptorSource material;
material.dwordCount = 4u;
material.dwords[2] = &readConst(7);
auto& wide = make(IrOpcode::Void, IrType::U64);
wide.SetImmediateU64(0x8000000000000001ull);
material.dwords[3] = &wide;
plan.descriptorSources = {buffer, table, heap, material};
plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}};
plan.materializationSources = {0u, 2u};
plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}};
plan.guardedSrtSlots = {1u};
plan.srtGuardOffset = 6u;
plan.cleanFlatSlots = {0u, 1u};
plan.pureFlatSlots = {1u, 0u};
plan.requiresSpecializationMemory = true;
plan.srtPlanComplete = true;
plan.resourceTrackingComplete = true;
plan.info.buffers = {BufferResource{}, BufferResource{2u, 0x40u, 0x100u, 0u, true, true, true, true, true, 0x7u, true, 4u}};
ImageResource image{};
image.source = 1u;
image.resourceClass = ImageResourceClass::Sampled;
image.dimension = RdnaImageDimension::Dim2D;
plan.info.images = {image};
ResourceMaterializer::PrepareImageModes(plan.info);
plan.uniformFill.fill = {UniformFillKind::Buffer, 1u, {4u, 8u, 12u}, 2u, 0x3f800000u};
auto& fillValue = readConst(5);
plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr};
return plan;
}
std::vector<std::byte> encodedPlan(const IrResourcePlan& plan) {
std::vector<std::byte> bytes;
require(ShaderDiskCache::EncodePlan(plan, bytes), "a self-contained plan does not encode");
return bytes;
}
bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) {
if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false;
std::map<const IrValue*, std::size_t> leftValues;
std::map<const IrValue*, std::size_t> rightValues;
std::map<const IrBlock*, std::size_t> leftBlocks;
std::map<const IrBlock*, std::size_t> rightBlocks;
for (std::size_t index = 0; index < left.valueStorage.size(); ++index) {
leftValues.emplace(left.valueStorage[index].get(), index);
rightValues.emplace(right.valueStorage[index].get(), index);
}
for (std::size_t index = 0; index < left.blockStorage.size(); ++index) {
leftBlocks.emplace(left.blockStorage[index].get(), index);
rightBlocks.emplace(right.blockStorage[index].get(), index);
}
const auto sameValue = [&](const IrValue* a, const IrValue* b) {
if (a == nullptr || b == nullptr) return a == b;
const auto foundA = leftValues.find(a);
const auto foundB = rightValues.find(b);
return foundA != leftValues.end() && foundB != rightValues.end() && foundA->second == foundB->second;
};
const auto sameBlock = [&](const IrBlock* a, const IrBlock* b) {
const auto foundA = leftBlocks.find(a);
const auto foundB = rightBlocks.find(b);
return foundA != leftBlocks.end() && foundB != rightBlocks.end() && foundA->second == foundB->second;
};
for (std::size_t index = 0; index < left.blockStorage.size(); ++index) {
if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false;
}
for (std::size_t index = 0; index < left.valueStorage.size(); ++index) {
const auto& a = *left.valueStorage[index];
const auto& b = *right.valueStorage[index];
if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags<std::uint64_t>() != b.Flags<std::uint64_t>() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false;
if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false;
for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) {
if (!sameValue(a.Argument(operand), b.Argument(operand))) return false;
}
for (std::size_t operand = 0; operand < a.PhiBlockCount(); ++operand) {
if (!sameBlock(a.PhiBlock(operand), b.PhiBlock(operand))) return false;
}
}
if (left.stage != right.stage || left.shaderHash != right.shaderHash || left.userDataBase != right.userDataBase || left.userDataCount != right.userDataCount || left.srgbDecodeFormats != right.srgbDecodeFormats) return false;
if (left.memoryInfo != right.memoryInfo || left.descriptorSources.size() != right.descriptorSources.size() || left.controlFlow.size() != right.controlFlow.size() || left.srtReads.size() != right.srtReads.size()) return false;
for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) {
const auto& a = left.descriptorSources[index];
const auto& b = right.descriptorSources[index];
if (a.dwordCount != b.dwordCount || a.indirectImage != b.indirectImage) return false;
for (std::size_t dword = 0; dword < a.dwords.size(); ++dword) {
if (!sameValue(a.dwords[dword], b.dwords[dword])) return false;
}
}
for (std::size_t index = 0; index < left.controlFlow.size(); ++index) {
const auto& a = left.controlFlow[index];
const auto& b = right.controlFlow[index];
if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false;
}
for (std::size_t index = 0; index < left.srtReads.size(); ++index) {
if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false;
}
for (std::size_t index = 0; index < left.uniformFill.values.size(); ++index) {
if (!sameValue(left.uniformFill.values[index], right.uniformFill.values[index])) return false;
}
return left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots &&
left.requiresSpecializationMemory == right.requiresSpecializationMemory && left.srtPlanComplete == right.srtPlanComplete && left.resourceTrackingComplete == right.resourceTrackingComplete && left.info == right.info && left.uniformFill.fill == right.uniformFill.fill;
}
struct MaterializedPlan {
bool ok = false;
std::string error;
ResourceSnapshot snapshot;
SrtReadTrace trace;
std::size_t leaf = 0;
};
MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) {
MaterializedPlan outcome;
runtime.readTrace = &outcome.trace;
try {
ResourceMaterializer{}.Materialize(plan, runtime, outcome.snapshot);
outcome.ok = true;
} catch (const std::exception& error) {
outcome.error = error.what();
}
outcome.leaf = static_cast<std::size_t>(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return value.get() == outcome.trace.leaf; }) - plan.valueStorage.begin());
return outcome;
}
bool sameMaterialization(const MaterializedPlan& left, const MaterializedPlan& right) {
const auto& a = left.snapshot;
const auto& b = right.snapshot;
return left.ok == right.ok && left.error == right.error && a.buffers == b.buffers && a.images == b.images && a.samplers == b.samplers && a.flattenedSrt == b.flattenedSrt && a.userData == b.userData && a.uniformFill == b.uniformFill && a.srtPoison == b.srtPoison && a.nullRootReads == b.nullRootReads &&
left.leaf == right.leaf && left.trace.leafSlot == right.trace.leafSlot && left.trace.leaves == right.trace.leaves && left.trace.otherReads == right.trace.otherReads;
}
void mutateWords(std::vector<std::uint32_t>& words, std::mt19937& random) {
static constexpr std::array<std::uint64_t, 8> addresses{PlanSrtAddress, PlanTableAddress, PlanMaterialAddress, PlanPayloadAddress, PlanPointerAddress, PlanOutputAddress, PlanTextureAddress, PlanUnmappedAddress};
for (auto& word : words) {
switch (random() % 16u) {
case 0: word = static_cast<std::uint32_t>(random()); break;
case 1: word = 0u; break;
case 2: {
const auto address = addresses[random() % addresses.size()] + (random() % 8u) * 4u;
word = random() % 2u == 0u ? static_cast<std::uint32_t>(address) : static_cast<std::uint32_t>(address >> 32u);
break;
}
case 3: word ^= 1u << (random() % 32u); break;
default: break;
}
}
}
std::size_t verifyRoundTripRuntimes(const IrResourcePlan& plan, const IrResourcePlan& decoded, const PlanFixture& fixture, std::uint32_t seed, const std::string& what) {
std::mt19937 random(seed);
std::size_t succeeded = 0;
for (std::uint32_t run = 0; run < 1000u; ++run) {
auto userData = fixture.userData;
auto regions = fixture.regions;
if (userData.size() < plan.userDataCount) userData.resize(plan.userDataCount, 0u);
if (run != 0u) {
mutateWords(userData, random);
for (auto& region : regions) mutateWords(region.second, random);
}
std::vector<MemoryRegion> memory;
for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))});
RequestMemoryView view(memory);
const auto runtime = view.MakeRuntime(userData, fixture.codeAddress);
const auto original = materializePlan(plan, runtime);
const auto loaded = materializePlan(decoded, runtime);
require(sameMaterialization(original, loaded), what + ": runtime " + std::to_string(run) + " materialized differently from the decoded plan (" + original.error + " / " + loaded.error + ")");
if (original.ok) ++succeeded;
}
return succeeded;
}
void verifyPlanEntryDamage(const std::vector<std::byte>& key, const IrResourcePlan& plan, const std::string& what) {
const auto file = ShaderDiskCache::EncodePlanEntry(key, plan);
require(!file.empty(), what + ": the plan entry was not encoded");
IrResourcePlan decoded;
require(ShaderDiskCache::DecodePlanEntry(file, key, decoded) == ShaderDiskCache::LoadStatus::Loaded && encodedPlan(decoded) == encodedPlan(plan), what + ": the plan entry does not decode to the plan");
for (std::size_t size = 0; size < file.size(); size += size < 512 ? 1 : 37) {
IrResourcePlan partial;
require(ShaderDiskCache::DecodePlanEntry(std::span(file).first(size), key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry truncated to " + std::to_string(size) + " bytes is not rejected");
}
for (std::size_t offset = 0; offset < file.size(); offset += offset < 512 ? 1 : 29) {
for (const auto bit : {std::byte{0x01}, std::byte{0x80}}) {
auto damaged = file;
damaged[offset] ^= bit;
IrResourcePlan partial;
require(ShaderDiskCache::DecodePlanEntry(damaged, key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with byte " + std::to_string(offset) + " flipped is not rejected");
}
}
auto longer = file;
longer.push_back(std::byte{0});
require(ShaderDiskCache::DecodePlanEntry(longer, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with a trailing byte is not rejected");
auto otherKey = key;
otherKey.back() ^= std::byte{1};
require(ShaderDiskCache::DecodePlanEntry(file, otherKey, decoded) == ShaderDiskCache::LoadStatus::KeyMismatch, what + ": a plan entry for another key loads");
auto previousFormat = file;
previousFormat[4] = static_cast<std::byte>(ShaderDiskCache::FormatVersion - 1u);
require(ShaderDiskCache::DecodePlanEntry(previousFormat, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of the previous format loads");
auto otherSource = file;
otherSource[8] ^= std::byte{1};
require(ShaderDiskCache::DecodePlanEntry(otherSource, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of another source version loads");
CompiledVariant variant;
require(ShaderDiskCache::DecodeEntry(file, key, variant) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry decodes as a compiled variant");
}
void verifyPlanPayloadDamage(const IrResourcePlan& plan, const std::string& what) {
const auto bytes = encodedPlan(plan);
for (std::size_t size = 0; size < bytes.size(); ++size) {
IrResourcePlan partial;
require(!ShaderDiskCache::DecodePlan(std::span(bytes).first(size), partial), what + ": a plan truncated to " + std::to_string(size) + " bytes decodes");
}
std::size_t rejected = 0;
for (std::size_t offset = 0; offset < bytes.size(); ++offset) {
for (const auto bit : {std::byte{0x01}, std::byte{0x40}}) {
auto damaged = bytes;
damaged[offset] ^= bit;
IrResourcePlan decoded;
if (!ShaderDiskCache::DecodePlan(damaged, decoded)) {
++rejected;
continue;
}
std::vector<std::byte> again;
require(ShaderDiskCache::EncodePlan(decoded, again), what + ": a plan accepted with byte " + std::to_string(offset) + " flipped is not self-contained");
}
}
require(rejected != 0u, what + ": no damaged plan was rejected");
}
void verifyMalformedPlans() {
const auto rejects = [](const std::string& what, const std::function<void(IrResourcePlan&)>& damage) {
auto plan = handBuiltPlan();
damage(plan);
std::vector<std::byte> bytes;
require(ShaderDiskCache::EncodePlan(plan, bytes), "malformed plan: " + what + " did not encode");
IrResourcePlan decoded;
require(!ShaderDiskCache::DecodePlan(bytes, decoded), "malformed plan: " + what + " decodes");
};
rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); });
rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; });
rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 4u; });
rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; });
rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(4u); });
rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 4u; });
rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; });
rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; });
rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; });
rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->heapSource = 4u; });
rejects("an image table material past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->materialSource = 4u; });
auto foreign = handBuiltPlan();
IrValue stray(IrOpcode::Void, IrType::U32, 99u);
foreign.srtReads[0].value = &stray;
std::vector<std::byte> bytes;
require(!ShaderDiskCache::EncodePlan(foreign, bytes), "a plan reading a value it does not own was encoded");
auto unblocked = handBuiltPlan();
auto& phi = *unblocked.valueStorage.emplace_back(std::make_unique<IrValue>(IrOpcode::Phi, IrType::U32, 100u));
phi.AddArgument(unblocked.valueStorage[0].get());
unblocked.srtReads[0].value = &phi;
bytes.clear();
require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded");
auto emptied = handBuiltPlan();
emptied.valueStorage.push_back(nullptr);
bytes.clear();
require(!ShaderDiskCache::EncodePlan(emptied, bytes), "a plan with an empty value slot was encoded");
}
void verifyPlanRoundTrip() {
const auto handBuilt = handBuiltPlan();
const auto handBytes = encodedPlan(handBuilt);
IrResourcePlan handDecoded;
require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode");
require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.size() == handBuilt.valueStorage.size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip");
const auto& phi = *handDecoded.descriptorSources[0].dwords[2];
require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands");
require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values");
require(samePlan(handBuilt, handDecoded), "the decoded hand-built plan differs from the plan in a field or a value");
PlanFixture handFixture;
handFixture.userData = {static_cast<std::uint32_t>(PlanSrtAddress), static_cast<std::uint32_t>(PlanSrtAddress >> 32u), 0u, 0u};
std::vector<std::uint32_t> handSrt(16, 0u);
place(handSrt, 0, bufferWords(PlanOutputAddress, 0u, 16u));
handFixture.regions = {{PlanSrtAddress, handSrt}, {PlanPayloadAddress, {0x3f800000u}}};
static_cast<void>(verifyRoundTripRuntimes(handBuilt, handDecoded, handFixture, 7u, "hand-built plan"));
verifyPlanPayloadDamage(handBuilt, "hand-built plan");
verifyMalformedPlans();
auto fixtures = planFixtures();
std::vector<std::shared_ptr<const IrResourcePlan>> plans;
std::size_t index = 0;
for (auto& fixture : fixtures) {
auto request = fixture.Request(false);
const auto plan = GetResourcePlan(request);
plans.push_back(plan);
const auto bytes = encodedPlan(*plan);
IrResourcePlan decoded;
require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode");
require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip");
require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value");
require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph");
const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast<std::uint32_t>(index), fixture.name);
require(succeeded != 0u, fixture.name + ": no runtime materialized");
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(request, 32u, key);
if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name);
std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n";
++index;
}
const auto covers = [&](const std::function<bool(const IrResourcePlan&)>& predicate) {
return std::any_of(plans.begin(), plans.end(), [&](const std::shared_ptr<const IrResourcePlan>& plan) { return predicate(*plan); });
};
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.indirectImage.has_value(); }); }), "no fixture plans an indirect image table");
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition");
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value->IsPhi(); }); }), "no fixture plans a phi");
require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read");
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot");
}
void verifyPlanKey() {
auto fixtures = planFixtures();
auto& fixture = fixtures[1];
const auto planKey = [&](const std::function<void(RecompileRequest&)>& change, std::uint32_t hostSubgroupSize = 32u) {
auto request = fixture.Request(true);
change(request);
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(request, hostSubgroupSize, key);
return key;
};
const auto key = planKey([](RecompileRequest&) {});
require(planKey([](RecompileRequest&) {}) == key, "the plan key is not deterministic");
std::vector<std::byte> variantKey;
ShaderDiskCache::BuildKey(fixture.Request(true), 32u, variantKey);
require(variantKey != key && ShaderDiskCache::EntryName(variantKey) != ShaderDiskCache::EntryName(key), "the plan and the compiled variant share an entry");
require(planKey([](RecompileRequest& request) { request.layout = {1u, 4u, 16u, 64u}; }) == key, "the binding layout changed the plan key");
require(planKey([](RecompileRequest& request) { request.shader.codeAddress += 0x1000u; request.context.memory = {}; }) == key, "the code address or the memory changed the plan key");
auto userData = fixture.userData;
userData[0] ^= 0x100u;
require(planKey([&](RecompileRequest& request) { request.context.userData = userData; }) == key, "user data values changed the plan key");
const auto original = fixture.code;
for (std::size_t word = 0; word < original.size(); ++word) {
for (std::uint32_t bit = 0; bit < 32u; ++bit) {
fixture.code = original;
fixture.code[word] ^= 1u << bit;
require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores code word " + std::to_string(word) + " bit " + std::to_string(bit));
}
}
fixture.code = original;
fixture.code.push_back(0xbf810000u);
require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores an appended code word");
fixture.code = original;
require(planKey([](RecompileRequest&) {}, 64u) != key, "the plan key ignores the host subgroup size");
require(planKey([](RecompileRequest& request) { request.target.subgroupSize = 64u; }) != key, "the plan key ignores the target");
require(planKey([](RecompileRequest& request) { request.target.srgbDecodeFormats = 2u; }) != key, "the plan key ignores the sRGB formats decoded in the shader");
require(planKey([](RecompileRequest& request) { request.context.waveSize = 64u; request.context.compute->numThreads[0] = 64u; }) != key, "the plan key ignores the wave size");
require(planKey([](RecompileRequest& request) { request.context.floatMode = ShaderFloatMode{0xc0u, true, false, false}; }) != key, "the plan key ignores the float mode");
}
void appendWords(std::vector<std::byte>& out, std::span<const std::uint32_t> words) {
const auto count = static_cast<std::uint64_t>(words.size());
const auto countBytes = std::as_bytes(std::span(&count, 1));
out.insert(out.end(), countBytes.begin(), countBytes.end());
const auto bytes = std::as_bytes(words);
out.insert(out.end(), bytes.begin(), bytes.end());
}
void appendSnapshot(std::vector<std::byte>& out, const ResourceCapture& capture) {
const auto& snapshot = capture.snapshot;
for (const auto* values : {&snapshot.buffers, &snapshot.images, &snapshot.samplers}) {
std::vector<std::uint32_t> words;
for (const auto& value : *values) {
words.push_back(value.dwordCount);
words.insert(words.end(), value.dwords.begin(), value.dwords.end());
}
appendWords(out, words);
}
appendWords(out, snapshot.flattenedSrt);
appendWords(out, snapshot.userData);
const auto& fill = snapshot.uniformFill;
appendWords(out, std::vector<std::uint32_t>{static_cast<std::uint32_t>(fill.kind), fill.resource, fill.groupStride[0], fill.groupStride[1], fill.groupStride[2], fill.words, fill.value, snapshot.nullRootReads});
std::vector<std::uint32_t> poison;
for (const auto& entry : snapshot.srtPoison) poison.insert(poison.end(), {entry.slot, entry.pc, static_cast<std::uint32_t>(entry.address), static_cast<std::uint32_t>(entry.address >> 32u)});
appendWords(out, poison);
std::vector<std::uint32_t> leaves{capture.readTrace.leafSlot};
for (const auto& [slot, address] : capture.readTrace.leaves) leaves.insert(leaves.end(), {slot, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)});
appendWords(out, leaves);
std::vector<std::uint32_t> reads;
for (const auto address : capture.readTrace.otherReads) reads.insert(reads.end(), {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)});
appendWords(out, reads);
}
std::vector<std::vector<std::byte>> preparedOutputs(std::vector<PlanFixture>& fixtures, std::vector<std::shared_ptr<const IrResourcePlan>>* plans = nullptr) {
std::vector<std::vector<std::byte>> outputs;
for (auto& fixture : fixtures) {
const auto request = fixture.Request(true);
const auto handle = PrepareShader(request);
RequestMemoryView view(request.context.memory);
const auto capture = CaptureResources(request, view.MakeRuntime(request.context.userData, request.shader.codeAddress), *handle);
const auto result = MaterializeShader(request, *capture, *handle);
std::vector<std::byte> bytes;
appendWords(bytes, GetPreparedArtifact(*handle).spirv.Words());
ShaderDiskCache::EncodeResult(*result, bytes);
appendSnapshot(bytes, *capture);
require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode");
if (plans != nullptr) plans->push_back(capture->plan);
outputs.push_back(std::move(bytes));
}
return outputs;
}
std::filesystem::path planOutputPath(std::size_t index) {
return ShaderCacheDirectory() / "plan-outputs" / ("fixture-" + std::to_string(index) + ".bin");
}
std::filesystem::path planEntryPath(PlanFixture& fixture) {
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(fixture.Request(true), 32u, key);
return ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key);
}
int runPlanLoadingProcess(std::string_view mode) {
if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1");
if (mode == "--load-plan-off") setEnvironment("APS5_NO_PERF_DISK_PLANS", "1");
auto fixtures = planFixtures();
const auto count = fixtures.size();
std::vector<std::shared_ptr<const IrResourcePlan>> plans;
const auto outputs = preparedOutputs(fixtures, &plans);
ShaderDiskCache::Flush();
const auto totals = ShaderDiskCache::Totals();
const auto builds = FrontEndBuilds();
const auto counts = " (front end builds " + std::to_string(builds) + ", plan hits " + std::to_string(totals.planHits) + ", plan misses " + std::to_string(totals.planMisses) + ", plan load failures " + std::to_string(totals.planLoadFailures) + ", variant hits " + std::to_string(totals.hits) + ", variant misses " + std::to_string(totals.misses) + ")";
if (mode == "--load-plan-switch") {
require(builds == count && totals.planHits == 0u && totals.planMisses == count && totals.misses == count, "another switch value reused stored plans or variants" + counts);
std::cout << "a process with another switch value rebuilt all " << count << " plans\n";
return 0;
}
if (mode == "--load-plan") {
require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts);
} else if (mode == "--load-plan-off") {
require(builds == count && totals.planHits == 0u && totals.planMisses == 0u && totals.planWrites == 0u && totals.hits == count && totals.misses == 0u, "APS5_NO_PERF_DISK_PLANS=1 used the stored plans or missed the stored variants" + counts);
} else {
require(builds == 1u && totals.planHits == count - 1u && totals.planLoadFailures == 1u && totals.planWrites == 1u && totals.hits == count && totals.misses == 0u, "the damaged plan was not rejected and rebuilt alone" + counts);
}
for (std::size_t index = 0; index < count; ++index) {
std::vector<std::byte> expected;
require(ReadWholeFile(planOutputPath(index), expected), "cannot read the first process's output for " + fixtures[index].name);
require(outputs[index] == expected, fixtures[index].name + ": the SPIR-V, bindings, push constants, snapshot or plan differ from the first process");
}
if (mode == "--load-plan") {
auto changed = planFixtures();
auto& fixture = changed.front();
fixture.code.insert(fixture.code.end() - 1, 0xbf800000u);
const auto request = fixture.Request(true);
static_cast<void>(PrepareShader(request));
const auto after = ShaderDiskCache::Totals();
require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan");
std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n";
} else if (mode == "--load-plan-off") {
std::cout << "APS5_NO_PERF_DISK_PLANS=1 ran the front end for all " << count << " shaders with the same output\n";
} else {
std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n";
}
for (std::size_t index = 0; index < count; ++index) {
const auto fresh = GetResourcePlan(fixtures[index].Request(false));
require(samePlan(*plans[index], *fresh), fixtures[index].name + ": the plan this process used differs from the plan its front end builds");
}
ShaderDiskCache::Flush();
return 0;
}
void verifyPlanAcrossProcesses(const char* self) {
auto fixtures = planFixtures();
const auto builds = FrontEndBuilds();
const auto before = ShaderDiskCache::Totals();
const auto outputs = preparedOutputs(fixtures);
ShaderDiskCache::Flush();
const auto after = ShaderDiskCache::Totals();
require(FrontEndBuilds() - builds == fixtures.size() && after.planMisses - before.planMisses == fixtures.size() && after.planWrites - before.planWrites == fixtures.size(), "the first process did not build and store every plan");
std::filesystem::create_directories(planOutputPath(0).parent_path());
for (std::size_t index = 0; index < outputs.size(); ++index) require(WriteFileAtomically(planOutputPath(index), outputs[index]), "cannot write the first process's output");
auto refused = fixtures[1];
refused.codeAddress = 0x200000u;
refused.code = {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u};
bool threw = false;
try {
static_cast<void>(PrepareShader(refused.Request(true)));
} catch (const std::runtime_error&) {
threw = true;
}
ShaderDiskCache::Flush();
require(threw && ShaderDiskCache::Totals().planWrites == after.planWrites && !std::filesystem::exists(planEntryPath(refused)), "a plan the front end refused was stored");
std::uintmax_t planBytes = 0;
for (auto& fixture : fixtures) planBytes += std::filesystem::file_size(planEntryPath(fixture));
std::cout << fixtures.size() << " plan entries, " << planBytes / fixtures.size() << " bytes per entry on average\n";
const auto run = [&](const char* mode) {
const std::string command = "\"" + std::string(self) + "\" " + mode;
require(std::system(command.c_str()) == 0, std::string("the process ") + mode + " failed");
};
run("--load-plan");
run("--load-plan-off");
std::vector<std::byte> entry;
const auto damaged = planEntryPath(fixtures.front());
require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry");
entry[entry.size() / 2] ^= std::byte{0x20};
require(WriteFileAtomically(damaged, entry), "cannot damage a stored plan entry");
run("--load-plan-corrupt");
std::vector<std::byte> rewritten;
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(fixtures.front().Request(true), 32u, key);
IrResourcePlan plan;
require(ReadWholeFile(damaged, rewritten) && ShaderDiskCache::DecodePlanEntry(rewritten, key, plan) == ShaderDiskCache::LoadStatus::Loaded, "the rebuilt plan was not stored again");
run("--load-plan-switch");
}
void verifyDefaultDirectory(const char* self) {
setEnvironment("ANYPS5_SHADER_CACHE_DIR", "");
setEnvironment("ANYPS5_NO_SHADER_CACHE", "1");
@ -1098,6 +1843,7 @@ int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]);
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}()));
std::filesystem::remove_all(directory);
verifyDefaultDirectory(argv[0]);
@ -1109,8 +1855,11 @@ int main(int argc, char** argv) {
verifyArtifactStorageIsolation();
verifyKeySensitivity();
verifyFloatModeSerialization();
verifyPlanRoundTrip();
verifyPlanKey();
verifyStore();
verifyAcrossProcesses(argv[0]);
verifyPlanAcrossProcesses(argv[0]);
verifyEmissionFailureMemo();
verifyFailureMemoSwitch(argv[0]);
verifyInvocationIsolation();

View File

@ -1686,6 +1686,23 @@ void verifyMeshConfiguration() {
other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}};
RecompileCacheKey::Build(other, key);
require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration");
RecompileRequest hull{};
hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}};
hull.context.waveSize = 64;
hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u}, {}};
const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull));
const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr;
require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u, "the quad tessellation configuration or its patches per group were lost in serialization");
auto grouped = hull;
grouped.graphics->tessellation->patchesPerGroup = 62u;
for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) {
hull.shader.stage = stage;
grouped.shader.stage = stage;
RecompileCacheKey::Build(hull, key);
const auto patchKey = key;
RecompileCacheKey::Build(grouped, key);
require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index");
}
const std::array<std::uint32_t, 8> users{};
request.context.waveSize = 32u;
request.context.userData = users;
@ -1825,14 +1842,16 @@ void verifyPixelRequestSerialization() {
minimal.context.waveSize = 64;
minimal.context.pixel = ShaderPixelStageInfo{};
minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
minimal.context.pixel->layerExported = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQRQAAAA=", "new requests did not use serialization version 20");
require(requestPrefix(encoded, 8u) == "NVNQQRcAAAA=", "new requests did not use serialization version 23");
require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip");
require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip");
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
for (std::size_t bytes = 0; bytes < 20u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted");
for (std::size_t bytes = 0; bytes < 22u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class or layer export flag was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRUAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRgAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@ -1921,6 +1940,14 @@ void verifyVertexInfoVersion19() {
while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset;
require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds");
require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it");
auto version20 = flagged;
version20[4] = 20u;
const auto replay20 = serializer.Deserialize(encodeBase64(version20));
require(replay20.request.context.vertex.has_value() && replay20.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a version 20 vertex info lost its position export layout");
require(serializer.Serialize(replay20.request) == marked, "a version 20 vertex info misread the fields after its position export layout");
vertex.context.vertex->paClVsOutCntl = (1u << 18u) | (1u << 21u) | 0xfu;
const auto layered = serializer.Deserialize(serializer.Serialize(vertex));
require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index");
auto legacy = plain;
legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u);
for (const auto version : {16u, 17u, 18u, 19u}) {
@ -1935,11 +1962,20 @@ void verifyVertexInfoVersion19() {
pixel.context.pixel = ShaderPixelStageInfo{};
pixel.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
const auto encodedPixel = serializer.Serialize(pixel);
auto exportedPixel = pixel;
exportedPixel.context.pixel->layerExported = true;
const auto exportedBytes = decodeBase64(serializer.Serialize(exportedPixel));
auto legacyPixel = decodeBase64(encodedPixel);
legacyPixel[4] = 19u;
const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel));
require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "a main version 19 request lost its pixel primitive class");
require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 request misread the fields after its pixel primitive class");
std::size_t flagOffset = 0;
while (flagOffset < legacyPixel.size() && legacyPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset;
require(flagOffset < legacyPixel.size(), "the pixel layer export flag is not serialized");
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset));
for (const auto version : {19u, 20u, 21u, 22u}) {
legacyPixel[4] = static_cast<std::uint8_t>(version);
const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel));
require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported, "a main version 19 to 22 request lost its pixel primitive class or read a layer export flag");
require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 to 22 request misread the fields after its pixel primitive class");
}
}
void verifyLegacyPixelRequests() {
@ -2034,7 +2070,7 @@ void verifyLegacyPixelRequests() {
require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader");
require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned");
require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned");
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread");
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules && !request.target.float16DenormPreserve, "legacy request trailer was misread");
const auto upgraded = serializer.Deserialize(serializer.Serialize(request));
require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping");
}
@ -2455,6 +2491,40 @@ void verifySpecializedBufferAtomicBindings() {
expectFailure([&] { static_cast<void>(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64");
}
void verifyCoherentGds() {
using namespace ShaderRecompiler;
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
const std::vector<std::uint32_t> code{0x34000082u, 0xbefc03ffu, 0x0000ffffu, 0xd8da0000u, 0x01000000u, 0xbf8cc07fu, 0xd8360080u, 0x00000100u, 0xbf810000u};
RecompileRequest request{};
request.shader = {ShaderStage::Compute, 0x5c000u, code, 0, {}};
request.context.waveSize = 32;
request.context.userDataBaseRegister = 0;
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 32;
request.target.bdaAbiVersion = BdaAbi::Version;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.fragmentShaderBarycentricEnabled = false;
request.layout.pushConstantSizeBytes = 128;
request.useCache = false;
const auto words = Recompile(request).spirv;
std::uint32_t gds = 0u;
std::map<std::uint32_t, std::vector<std::uint32_t>> decorations;
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
const auto opcode = words[cursor] & 0xffffu;
require(count != 0 && count <= words.size() - cursor, "coherent GDS: truncated SPIR-V instruction");
if (opcode == spv::OpName && std::string_view(reinterpret_cast<const char*>(&words[cursor + 2])) == "gds") gds = words[cursor + 1];
if (opcode == spv::OpDecorate) decorations[words[cursor + 1]].push_back(words[cursor + 2]);
cursor += count;
}
require(gds != 0u, "coherent GDS: a ds_read_b32 and ds_write_b32 on GDS declare no gds buffer");
require(std::ranges::find(decorations[gds], static_cast<std::uint32_t>(spv::DecorationCoherent)) != decorations[gds].end(), "coherent GDS: the gds buffer is not Coherent");
}
void verifyCoherentBufferAccesses() {
using namespace ShaderRecompiler;
alignas(256) static std::array<std::uint32_t, 64> input{};
@ -3529,6 +3599,7 @@ int main(int argc, char** argv) {
verifyGuardedNullPointers();
verifyCubeDepthCompareMode();
verifySrgbDepthCompareModes();
verifyCoherentGds();
#if ANYPS5_ENABLE_SPIRV_TOOLS
const std::vector<std::uint32_t> minimalSpirv{
0x07230203u, 0x00010000u, 0u, 5u, 0u,
@ -3642,6 +3713,9 @@ int main(int argc, char** argv) {
RecompileCacheKey::Build(uncached, plainKey);
RecompileCacheKey::Build(registrationModules, registrationKey);
require(plainKey != registrationKey, "registration modules did not change the cache key");
auto float16Denormals = uncached;
float16Denormals.target.float16DenormPreserve = true;
require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float16Denormals)).request.target.float16DenormPreserve, "f16 denormal preservation was lost in serialization");
auto changedLayout = request;
changedLayout.layout.pushConstantSizeBytes = 64;
require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant");

View File

@ -2,6 +2,7 @@
#include "SpirvBackend/SpirvSpecialization.hpp"
#include <array>
#include <cstdio>
#include <exception>
#include <map>
#include <set>
#include <string>
@ -285,8 +286,21 @@ void testFoldingLeavesNoDeadBlocksOrConstants() {
checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids");
}
void testIdsBeyondTheBoundAreRejected() {
auto words = foldedSwitchModule(13u, {1u, 1u, 1u});
words[3] = 12u;
bool rejected = false;
try {
static_cast<void>(ShaderRecompiler::SpecializeSpirv(words));
} catch (const std::exception&) {
rejected = true;
}
check(rejected, "a module whose ids reach its bound was specialized");
}
int main() {
testFoldingLeavesNoDeadBlocksOrConstants();
testIdsBeyondTheBoundAreRejected();
testStructExtractsAreNotShuffled();
testRepeatedTypesShareOneId();
testOperandShapesDoNotCollide();

View File

@ -10,6 +10,7 @@
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libSceAgcDriver/Eq/include/Query.hpp"
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include <array>
#include <algorithm>
@ -211,6 +212,22 @@ void testEndOfPipeInterrupts() {
check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt");
struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this<Output> {
std::atomic<int> ready{0};
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); }
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ready.fetch_add(1); }
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
};
const auto output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
std::array<std::uint32_t, 6> flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0};
Packet flipPacket{flip.data(), static_cast<std::uint32_t>(flip.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed");
AgcDriverWaitIdle_nid_postfix();
check(output->ready.load() == 1, "flip was not handed to its output");
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared");
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
alignas(8) static volatile std::uint64_t label = 0;
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&label);
words = {0xc0064900, 0x528, (3u << 29u) | (3u << 24u) | (1u << 16u), static_cast<std::uint32_t>(labelAddress), static_cast<std::uint32_t>(static_cast<std::uint64_t>(labelAddress) >> 32u), 0x89abcdefu, 0x01234567u, 0};

View File

@ -1,6 +1,7 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
#include <array>
@ -48,6 +49,20 @@ void srgbDecodeTests() {
Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views");
}
void srgbTwinTests() {
constexpr std::uint32_t srgb8 = 1u;
constexpr std::uint32_t srgb8_8 = 2u;
Context context{};
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB && SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_R8G8_SRGB, "a device that samples R8_SRGB and R8G8_SRGB must give R8 and R8G8 their sRGB twins");
context.srgbDecodeFormats = srgb8_8;
Require(SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_8_SRGB in the shader must not give R8G8 an sRGB twin");
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB, "R8 must keep its sRGB twin when only 8_8_SRGB is decoded in the shader");
context.srgbDecodeFormats = srgb8 | srgb8_8;
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin");
Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins");
Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin");
}
template<typename TAction>
void reject(TAction action, std::string_view reason) {
try {
@ -125,10 +140,28 @@ void singleDccKeysTests() {
Require(read(56) == DccKeys::Uncompressed, "comp-to-single keys mixed with uncompressed ones must read as stored");
}
void packedFloatDccClearTests() {
for (const bool alphaOnMsb : {false, true}) {
for (const auto keys : {DccKeys::Clear0000, DccKeys::Clear0001, DccKeys::Clear1110, DccKeys::Clear1111}) {
std::array<std::uint32_t, 4> texels{};
texels.fill(0xdeadbeefu);
Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, keys, alphaOnMsb, std::as_writable_bytes(std::span(texels))), "packed float fast clear was not encoded");
const std::uint32_t expected = keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 ? 0x781e03c0u : 0u;
for (const auto texel : texels) Require(texel == expected, "packed float DCC clear did not encode RGB zero or one");
}
}
alignas(64) std::array<std::uint8_t, 4> keys{0xc0, 0xc0, 0xc0, 0xc0};
GuestTextureResource resource{};
resource.format = 36;
resource.dccAddress = reinterpret_cast<std::uint64_t>(keys.data());
Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys");
}
}
void RunTextureFormatTests() {
srgbDecodeTests();
srgbTwinTests();
packedChannelTests();
Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM");
Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM");
@ -136,6 +169,15 @@ void RunTextureFormatTests() {
Require(!IsBlockCompressed(1), "format 1 must not be block compressed");
Require(BlockWidth(1) == 1u && BlockHeight(1) == 1u, "format 1 must have a one-texel block");
Require(ResolveTextureFormat(50) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "format 50 must resolve to A2B10G10R10_UNORM");
Require(ResolveTextureFormat(51) == VK_FORMAT_A2B10G10R10_SNORM_PACK32, "format 51 must resolve to A2B10G10R10_SNORM");
Require(ResolveTextureFormat(54) == VK_FORMAT_A2B10G10R10_UINT_PACK32, "format 54 must resolve to A2B10G10R10_UINT");
Require(ResolveTextureFormat(55) == VK_FORMAT_A2B10G10R10_SINT_PACK32, "format 55 must resolve to A2B10G10R10_SINT");
for (const std::uint32_t format : {50u, 51u, 54u, 55u}) Require(BytesPerElement(format) == 4u && !IsBlockCompressed(format), "packed 10-bit format " + std::to_string(format) + " must be four bytes and not block compressed");
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SNorm) == ShaderRecompiler::IrTextureNumericClass::Float, "format 51 must sample as float");
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SInt) == ShaderRecompiler::IrTextureNumericClass::Sint, "format 55 must sample as signed integer");
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2UInt) == ShaderRecompiler::IrTextureNumericClass::Uint, "format 54 must sample as unsigned integer");
Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM");
Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide");
@ -175,4 +217,5 @@ void RunTextureFormatTests() {
for (const std::uint32_t format : {1u, 14u, 56u, 71u, 169u, 171u, 173u, 181u}) Require(!IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must not be an sRGB texture format");
convertedDccClearTests();
singleDccKeysTests();
packedFloatDccClearTests();
}

View File

@ -0,0 +1,95 @@
#include "Recompiler.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <string>
#include <string_view>
#include <vector>
namespace {
struct Module {
std::vector<std::uint32_t> capabilities;
std::vector<std::string> extensions;
bool layerOutput = false;
std::uint32_t version = 0;
};
Module inspect(const std::vector<std::uint32_t>& words) {
Module module;
if (words.size() > 1) module.version = words[1];
std::vector<std::uint32_t> layerIds;
for (std::size_t at = 5; at < words.size();) {
const auto length = words[at] >> 16u;
const auto opcode = words[at] & 0xffffu;
if (length == 0 || at + length > words.size()) break;
if (opcode == 17u && length == 2) module.capabilities.push_back(words[at + 1]);
if (opcode == 10u && length >= 2) module.extensions.emplace_back(reinterpret_cast<const char*>(&words[at + 1]));
if (opcode == 71u && length == 4 && words[at + 2] == 11u && words[at + 3] == 9u) module.layerOutput = true;
at += length;
}
return module;
}
bool has(const std::vector<std::uint32_t>& values, std::uint32_t value) {
for (const auto entry : values) {
if (entry == value) return true;
}
return false;
}
bool named(const std::vector<std::string>& values, std::string_view value) {
for (const auto entry : values) {
if (entry == value) return true;
}
return false;
}
Module recompile(std::uint32_t control, bool layerExtension) {
using namespace ShaderRecompiler;
static const std::array<std::uint32_t, 5> code{0xf80000cfu, 0x00000000u, 0xf80008dfu, 0x01010101u, 0xbf810000u};
static const std::array<std::uint32_t, 3> withExtension{1u, 32u, 5254u};
static const std::array<std::uint32_t, 2> without{1u, 32u};
ShaderVertexStageInfo vertex{};
vertex.paClVsOutCntl = control;
RecompileRequest request{};
request.shader = {ShaderStage::Vertex, 0x30000u, code, 0, {}};
request.context.waveSize = 64;
request.context.vertex = vertex;
request.target.vulkanVersion = layerExtension ? 0x00401000u : 0x00402000u;
request.target.spirvVersion = layerExtension ? 0x00010300u : 0x00010500u;
request.target.subgroupSize = 64;
if (layerExtension) request.target.supportedCapabilities = withExtension;
else request.target.supportedCapabilities = without;
request.layout.pushConstantSizeBytes = 128;
request.useCache = false;
return inspect(Recompile(request).spirv);
}
}
int main() {
try {
constexpr std::uint32_t layer = (1u << 18u) | (1u << 21u);
const auto extension = recompile(layer, true);
if (!extension.layerOutput || !has(extension.capabilities, 5254u) || has(extension.capabilities, 69u) || !named(extension.extensions, "SPV_EXT_shader_viewport_index_layer") || extension.version != 0x00010300u) {
std::fprintf(stderr, "a vertex layer export with VK_EXT_shader_viewport_index_layer: layer %d, version 0x%x\n", extension.layerOutput ? 1 : 0, extension.version);
return 1;
}
const auto core = recompile(layer, false);
if (!core.layerOutput || !has(core.capabilities, 69u) || has(core.capabilities, 5254u) || core.version != 0x00010500u) {
std::fprintf(stderr, "a vertex layer export without the extension: layer %d, version 0x%x\n", core.layerOutput ? 1 : 0, core.version);
return 1;
}
const auto ignored = recompile(0u, true);
if (ignored.layerOutput || has(ignored.capabilities, 5254u)) {
std::fprintf(stderr, "a misc export without USE_VTX_RENDER_TARGET_INDX wrote gl_Layer\n");
return 1;
}
} catch (const std::exception& error) {
std::fprintf(stderr, "unexpected error: %s\n", error.what());
return 1;
}
return 0;
}

View File

@ -562,6 +562,142 @@ void CheckUnwatchedImport() {
#endif
}
class PartlyCommitted {
public:
static constexpr std::size_t Bytes = 4 * Block;
PartlyCommitted() {
#ifdef _WIN32
memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(Bytes, Block), Bytes, Block));
#else
void* raw = mmap(nullptr, Bytes + Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
Require(raw != MAP_FAILED, "cannot reserve the partly committed range");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + Bytes != begin + Bytes + Block) munmap(reinterpret_cast<void*>(aligned + Bytes), begin + Block - aligned);
memory = reinterpret_cast<std::uint8_t*>(aligned);
#endif
Require(memory != nullptr, "cannot reserve the partly committed range");
}
~PartlyCommitted() {
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(memory, Bytes);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, Bytes);
#else
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, Bytes);
munmap(memory, Bytes);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes);
#endif
}
PartlyCommitted(const PartlyCommitted&) = delete;
PartlyCommitted& operator=(const PartlyCommitted&) = delete;
void Commit(std::size_t block) {
#ifdef _WIN32
GuestArena::GuestArenaCommit_nid_postfix(memory + block * Block, Block, PAGE_READWRITE, Block);
#else
Require(mmap(memory + block * Block, Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit a block of the partly committed range");
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + block * Block, Block);
#endif
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block);
}
void Decommit(std::size_t block) {
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(memory + block * Block, Block);
#else
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + block * Block, Block);
Require(mmap(memory + block * Block, Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit a block of the partly committed range");
#endif
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block);
}
std::uint8_t* Data() const { return memory; }
std::uint64_t Address(std::size_t block = 0) const { return reinterpret_cast<std::uint64_t>(memory) + block * Block; }
private:
std::uint8_t* memory = nullptr;
};
using Runs = std::vector<std::pair<std::uint64_t, std::uint64_t>>;
std::uint64_t CollectCommitted(const PartlyCommitted& range, Runs& runs) {
BumpCollectEpoch();
return CollectCommittedWrites(range.Address(), PartlyCommitted::Bytes, runs);
}
void CheckCommittedRanges() {
PartlyCommitted range;
const auto base = range.Address();
range.Commit(0);
std::memset(range.Data(), 0x11, Block);
Require(CollectWrites(base, PartlyCommitted::Bytes) == 0, "a partly committed range was collected as a whole");
Runs runs;
const Runs first{{base, base + Block}};
const auto synced = CollectCommitted(range, runs);
Require(synced != 0 && runs == first, "the committed part of a partly committed range was not collected");
Require(UnchangedCommittedSince(runs, synced), "a collected committed part reads as changed");
range.Data()[8] = 0x22;
const auto written = CollectCommitted(range, runs);
Require(written > synced && runs == first && !UnchangedCommittedSince(runs, synced) && UnchangedCommittedSince(runs, written), "a CPU write to the committed part was missed");
Require(MarkWritten(base + 16, 4) != 0 && !UnchangedCommittedSince(first, written), "a driver store into the committed part was missed");
Require(UnchangedCommittedSince(first, CollectCommitted(range, runs)), "a collect after a driver store reads the committed part as changed");
const auto beforeCommit = CollectCommitted(range, runs);
range.Commit(2);
const Runs both{{base, base + Block}, {range.Address(2), range.Address(3)}};
const auto grown = CollectCommitted(range, runs);
Require(grown != 0 && runs == both, "a block committed inside the range is not described");
Require(!UnchangedCommittedSince(runs, beforeCommit), "a block committed inside the range reads as unchanged since before the commit");
Require(UnchangedCommittedSince(runs, grown), "the committed parts read as changed after their collect");
range.Decommit(2);
range.Commit(2);
const auto recommitted = CollectCommitted(range, runs);
Require(recommitted != 0 && runs == both && !UnchangedCommittedSince(runs, grown), "a block decommitted and committed again reads as unchanged");
range.Decommit(2);
Require(CollectCommitted(range, runs) != 0 && runs == first, "a decommitted block is still described as committed");
#ifdef _WIN32
HANDLE firstSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr);
HANDLE secondSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr);
struct Sections {
HANDLE first;
HANDLE second;
~Sections() {
if (first != nullptr) CloseHandle(first);
if (second != nullptr) CloseHandle(second);
}
} sections{firstSection, secondSection};
Require(firstSection != nullptr && secondSection != nullptr, "cannot create the shared backings of the partly committed range");
const auto beforeMap = CollectCommitted(range, runs);
GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, firstSection, 0, PAGE_READWRITE);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
const Runs viewed{{base, base + Block}, {range.Address(3), range.Address(4)}};
const auto mapped = CollectCommitted(range, runs);
Require(mapped != 0 && runs == viewed, "a view mapped inside the range is not described");
Require(!UnchangedCommittedSince(runs, beforeMap), "a view mapped inside the range reads as unchanged since before the mapping");
Require(UnchangedCommittedSince(runs, mapped), "a collected view reads as changed");
GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block);
GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, secondSection, 0, PAGE_READWRITE);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
Require(CollectCommitted(range, runs) != 0 && runs == viewed && !UnchangedCommittedSince(runs, mapped), "a view replaced inside the range reads as unchanged");
GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
Require(CollectCommitted(range, runs) != 0 && runs == first, "an unmapped view is still described as committed");
#endif
range.Decommit(0);
Require(CollectCommitted(range, runs) != 0 && runs.empty() && UnchangedCommittedSince(runs, TrackerGeneration()), "a range with no committed page did not collect as empty");
Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts");
}
int main() {
try {
if (!WriteWatched()) {
@ -587,6 +723,7 @@ int main() {
CheckCollectBoundaries();
CheckImportWindow();
CheckUnwatchedImport();
CheckCommittedRanges();
} catch (const std::exception& error) {
std::cerr << "write tracking test failed: " << error.what() << "\n";
return 1;

View File

@ -165,22 +165,23 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin
}
}
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode) {
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
cx[0x31b] = slice | (slice << 13u);
cx[0x31c] = Color32FloatInfo;
cx[0x31d] = 0;
cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (Layers - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u);
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array");
Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array");
else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
return color;
}
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode) {
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::size_t at = 0;
@ -207,7 +208,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
const auto color = DecodeTarget(surface, slice, swizzleMode);
const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed);
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
AgcDriver::Graphics::State state{};
@ -246,29 +247,31 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
device.WaitIdle();
}
void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) {
void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool collapsed = false, std::uint32_t slice = 1) {
const auto surface = AddressOf(texels);
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), SurfaceBytes / 4u, Initial);
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
const auto drawn = std::bit_cast<std::uint32_t>(Drawn);
DrawSlice(device, surface, 1, swizzleMode);
const auto pending = StorageTexture::FindPending(surface, SurfaceBytes);
Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array");
LoadLayer(device, surface, 1, swizzleMode);
RequireOutput(drawn, Initial, "slice 1 read through the array after the draw");
for (const std::uint32_t other : {0u, 2u, 3u}) {
DrawSlice(device, surface, slice, swizzleMode, collapsed);
const auto pending = StorageTexture::FindPending(collapsed ? surface + slice * LayerBytes : surface, collapsed ? LayerBytes : SurfaceBytes);
if (collapsed) Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == surface + slice * LayerBytes, "a collapsed slice did not render into its shifted surface");
else Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array");
LoadLayer(device, surface, slice, swizzleMode);
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " read through the array after the draw");
for (std::uint32_t other = 0; other < Layers; ++other) {
if (other == slice) continue;
LoadLayer(device, surface, other, swizzleMode);
RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1");
RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice " + std::to_string(slice));
}
StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test");
device.WaitIdle();
AgcDriver::Graphics::ClearCachedTextures(device.Device());
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
LoadLayer(device, surface, 1, swizzleMode);
RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory");
LoadLayer(device, surface, slice, swizzleMode);
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory");
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
@ -281,6 +284,7 @@ int main() {
if (!device) return VulkanTestSkipped;
Run(*device, block.Data(), TileR64KBX);
Run(*device, block.Data(), static_cast<std::uint32_t>(AgcDriver::Graphics::ColorTileMode::D64KBX));
for (std::uint32_t slice = 1; slice < Layers; ++slice) Run(*device, block.Data(), TileR64KBX, true, slice);
std::puts("array layer target tests passed");
return 0;
} catch (const std::exception& error) {

Some files were not shown because too many files have changed in this diff Show More