Skip to content

feat(libSceNgs2): support custom sampler rack and filter module - #4254

Closed
MarcZX10 wants to merge 3 commits into
boykopovar:mainfrom
MarcZX10:codex/carry-ngs2-custom-sampler-3759-current
Closed

MarcZX10 wants to merge 3 commits into
boykopovar:mainfrom
MarcZX10:codex/carry-ngs2-custom-sampler-3759-current

Conversation

@MarcZX10

@MarcZX10 MarcZX10 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds bounded support for the NGS2 custom sampler rack (0x4001) and carries an experimental custom-filter handler for module 0x13 using the 40-byte shadPS4 parameter layout and type 1. The filter implementation carries Jnbla6’s offered commit with its original author preserved; the rack implementation preserves screamm’s original commit author. New Windows runtime evidence in #4417 shows Dead Cells sends a 48-byte 0x40001300 parameter with a likely type value of 9 at +0x10 and frequency/Q/level-looking values starting at +0x14. The current 40-byte/type-1 handler does not cover that observed call, so this PR does not claim to unblock Dead Cells; the filter layout and type remain unresolved.

Tested

Linux: built ngs2_custom_tests and passed ctest -R '^ngs2_custom$' (1/1). The reported Dead Cells trace was run by another contributor on Windows against #4417 plus #4777; this PR was not applied in that run.

Checklist

  • Based on current main
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: feat(libSceNgs2.native): implement custom sampler rack id 0x4001 (#4334) #4417 (custom rack implementation changes overlap; rebase this filter follow-up after it lands)
  • AI-assisted: yes

@MarcZX10

Copy link
Copy Markdown
Contributor Author

Local validation on Linux, against the latest main used for the PR: clean Release build of ngs2_custom_tests and ctest -R "^ngs2_custom$" passed (1/1). PR CI is green for conventions, eligibility, intake, Ubuntu/macOS registration and write-watch checks, and Linux/macOS/Windows relinker jobs. The workflow skips the Linux and Windows full-suite jobs for this PR.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

PPSA15552 creates the 0x4001 rack with one built-in module, 0x13 (option dumps from xXPBSXx and hansjm10 on #3793), and Ngs2CheckCustomRack still throws custom module id 0x13 is not implemented for it, so the title stops in the same sceNgs2RackCreate. Can you implement module 0x13 from a source, or show what it does? As it is the title doesn't get further, and it sends no 0x4001xxxx voice params, so nothing reaches the sampler side either.

@Jnbla6

Jnbla6 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

@oneandonlydean @MarcZX100

the 0x13 module roadblock in Dead Cells (PPSA15552): I investigated search for it and found that module 0x13 is the built-in Custom Voice Filter (SCE_NGS2_CUSTOM_MODULE_ID_FILTER), and its 0x40001300 parameter updates map directly to AnyPS5's existing Biquad filter engine.

I have implemented solution for this in commit 1d5190d You can see and test if it unblock the startup crash, i didnt test it with run the game

@blawesom

Copy link
Copy Markdown
Contributor

Rebase handoff — rebased onto current main, authorship unchanged (only a TechnicalDebt.md union conflict; both entries kept).

Verified on Linux (CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON, AMD RX 9070 XT / RADV):

  • builds; tools/check_conventions.py --base origin/main passes
  • full suite: 100% passed

To pick it up:

git fetch https://github.com/blawesom/AnyPS5 rebase/4254
git checkout <your branch>
git merge --ff-only FETCH_HEAD

@MarcZX10
MarcZX10 force-pushed the codex/carry-ngs2-custom-sampler-3759-current branch from 5662961 to 07a6785 Compare October 12, 2026 16:11
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 12, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 12, 2026
@MarcZX10
MarcZX10 force-pushed the codex/carry-ngs2-custom-sampler-3759-current branch 2 times, most recently from c3f6cb6 to b0524d0 Compare October 12, 2026 18:48
@MarcZX10

Copy link
Copy Markdown
Contributor Author

Thanks for identifying 0x13 as the built-in Custom Voice Filter and offering 1d5190d. That commit is not in the current PR head b0524d0, and I have not run it.

Before carrying it, could you cite the source for the 40-byte parameter layout and the meanings of its type values? The proposed Ngs2CustomVoiceFilterParam declares both direct-coefficient and frequency/Q/level forms, but the handler only copies fields for type == 1. Other types pass their zero-initialized fields to Ngs2SetFilter; its current type-0 behavior clears the filter. If type 0 selects the direct-coefficient form, that path would be treated as a clear rather than implemented or rejected.

A run on PPSA15552 with this exact change would also show whether it gets past the reported sceNgs2RackCreate stop. The offered commit says it has not been tried in the title, so I am not claiming that it unblocks the game yet.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked on Linux: libSceNgs2.native build and its 9 ctest tests pass, ngs2_custom fails to build without the change. The code is scoped and throws on everything it doesn't model. The one open point is the one @oneandonlydean raised:

  • The PR body says it addresses the startup stop of #3759, but PPSA15552 creates the 0x4001 rack with module 0x13, and Ngs2CheckCustomRack still throws custom module id 0x13 is not implemented, so the title stops in the same sceNgs2RackCreate. Either implement 0x13 from a cited source (see the layout question to @Jnbla6 above: types other than 1 are not covered) with a test, or reword the body/debt line so they don't claim the title gets past this stop.
  • It also conflicts with main in docs/dev/debt/libSceNgs2.native.md (debt line adjacency); please rebase after the above.

@MarcZX10
MarcZX10 force-pushed the codex/carry-ngs2-custom-sampler-3759-current branch from b0524d0 to 64110c4 Compare October 13, 2026 17:20
@MarcZX10 MarcZX10 changed the title feat(libSceNgs2): accept the custom sampler rack feat(libSceNgs2): support custom sampler rack and filter module Oct 13, 2026
@MarcZX10

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (61b8742b6). This update adds the trace-observed custom filter module 0x13 for filter type 1, carrying Jnbla6’s offered commit with its author preserved; unsupported filter types throw explicitly. On Linux, ngs2_custom_tests built and ctest -R '^ngs2_custom$' passed (1/1), and repository conventions passed. Dead Cells was not run, so this does not claim title validation. The overlapping #4417 remains open.

@Jnbla6

Jnbla6 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Thanks for identifying 0x13 as the built-in Custom Voice Filter and offering 1d5190d. That commit is not in the current PR head b0524d0, and I have not run it.

Before carrying it, could you cite the source for the 40-byte parameter layout and the meanings of its type values? The proposed Ngs2CustomVoiceFilterParam declares both direct-coefficient and frequency/Q/level forms, but the handler only copies fields for type == 1. Other types pass their zero-initialized fields to Ngs2SetFilter; its current type-0 behavior clears the filter. If type 0 selects the direct-coefficient form, that path would be treated as a clear rather than implemented or rejected.

A run on PPSA15552 with this exact change would also show whether it gets past the reported sceNgs2RackCreate stop. The offered commit says it has not been tried in the title, so I am not claiming that it unblocks the game yet.

Source of the 40-Byte Layout
The layout is derived by directly cross-referencing shadPS4's reversed headers with prosper's live memory dumps from Dead Cells.

shadPS4 Reference: In shadPS4's src/core/libraries/ngs2/ngs2_custom.h, the struct OrbisNgs2CustomVoiceFilterParam is defined exactly as a 40-byte (0x28) structure. The offsets perfectly align with the static_asserts added in my commit: type at +0x08, channelMask at +0x0c, and the union param at +0x10.
https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h

Prosper Live Dump: When Dead Cells runs in prosper (which initially misidentified 0x40001300 as a MatrixLevels param), its trace log dumped the payload at these exact offsets. At hle_audio.cpp (around line 3914), the log reads ~1e4 (10000.0f) at +0x14, 0.707f at +0x18, and 1.0f at +0x1c. This perfectly maps to the fcq struct inside the union (frequency at +0x10, q at +0x14, level at +0x18 relative to the struct start).
https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp

  1. The Meaning of type values and why type == 0 (Clear/Bypass) is Correct
    In SCE audio architectures (both standard NGS2 and Custom modules), type == 0 universally means Bypass / Clear / Off.

The direct coefficients (i0, i1, i2, o1, o2) are not activated by type == 0. They are typically mapped to higher enum values (e.g., type == 5 or type == 6 depending on the exact SCE filter implementation).

Why this implementation is safe: If you look at AnyPS5's existing standard filter handler in Voice.cpp (Ngs2SetFilter), it explicitly states: if (param.type == 0) { filter = {}; return; }.

By falling back to passing type = 0 (or leaving fields zero-initialized when type != 1) to Ngs2SetFilter, we are safely forcing a Clear/Bypass. This is the exact intended and safe behavior for an unsupported or disabled filter type, avoiding garbage direct-coefficient processing while preventing a hard crash.

@MarcZX10

Copy link
Copy Markdown
Contributor Author

Thanks, the shadPS4 header is good evidence for the 0x28-byte struct and the type/channelMask/union offsets. I checked the cited prosper source: it defines 0x40001300 as kNgs2ParamMatrixLevels (line 3841), and parses the observed +0x18/+0x1c floats as output gains (line 3913 onward). That trace therefore supports the matrix-level interpretation, but does not independently validate the filter fcq fields or type semantics.

The remaining gap is the assertion that type 0 is the console's clear/bypass value (and that other unimplemented types should be passed through as a zero-initialized clear). Ngs2SetFilter establishes AnyPS5's existing behavior, but not the console ABI. Do you have a source for the custom-filter enum or a trace from the filter parameter ID showing type 0? If not, I suggest keeping type 0 handling as a deliberate compatibility assumption in the debt note and making unsupported nonzero types explicit instead of silently turning them into a clear.

@p3n1cillin

Copy link
Copy Markdown

Dead Cells (PPSA15552) runtime results are in #4417 (comment). In short, the title sends 0x40001300 with header size = 48, and its fc/Q/level (10000, 0.7071, 1.0) sit at +0x14, one word after shadPS4's fcq, with 9 at +0x10. So the type reads as 0 under the 40-byte layout and both PRs throw on it. After that, sceNgs2VoiceGetState is called on custom sampler voices with state_size = 48.

@MarcZX10

Copy link
Copy Markdown
Contributor Author

New on-title evidence from #4417 changes the filter analysis. The PS5 call uses a 48-byte payload: type appears to be 9 at +0x10, with frequency/Q/level-looking values at +0x14..+0x1c. This does not match the 40-byte shadPS4 layout assumed by the current implementation, and the game still stops at this custom filter after #4417 and #4777 are applied.

I have corrected my earlier comment: prosper's 0x40001300 MatrixLevels classification is not authoritative for the console and conflicts with this runtime trace. I will keep this PR open behind #4417, but its filter path needs a verified 48-byte layout and type-9 meaning before it can be treated as handling the reported title.

@MarcZX10

Copy link
Copy Markdown
Contributor Author

I updated the PR description and technical-debt entry to make the scope explicit: this change covers the bounded rack 0x4001 path and carries an experimental type-1 custom-filter handler based on the 40-byte shadPS4 layout; it does not implement the observed 48-byte/type-9 payload, and it does not claim to get Dead Cells past startup. The Windows title run in #4417 tested #4417 plus #4777, not this PR. Since #4417 remains open and these changes overlap, #4254 is still listed as dependent on it; I will rebase this follow-up after #4417 lands.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

#4417 is merged with your test fix from here. Could you rebase this onto main as the follow-up for the 48-byte type-9 filter call you traced (type at +0x10, the three values at +0x14..+0x1c), keeping only what #4417 doesn't cover? Type 9 throws on main now.

@MarcZX10

Copy link
Copy Markdown
Contributor Author

Correction after #4417 merged: current main already contains the custom sampler rack and the module 0x13 type-1 filter path carried by this PR, including rejection of other filter types. The new #4417 Dead Cells run confirms that type-1 handling still does not cover the observed 48-byte/type-9 payload. I am not rebasing this branch as a separate implementation because its code now duplicates merged behavior. I am leaving the PR open, but it should be treated as superseded unless a distinct, verified change is identified.

@MarcZX10

Copy link
Copy Markdown
Contributor Author

I tested the requested rebase in an isolated worktree. Git skips 64110c4fd because its test fix is already in main; the custom-sampler commit conflicts with the implementation merged in #4417, and the remaining type-1 filter change is already present in main. After carrying those merged changes, there is no distinct patch left to publish.

The captured type-9 call is still a separate gap: current main models a 40-byte filter parameter with type at +0x8 and frequency/Q/level at +0x10, while the title trace reports a 48-byte payload with type at +0x10 and those values at +0x14..+0x1c. Could you share the field interpretation/source for the extra words and the meaning of type 9? I can’t safely implement the type-9 path from the offsets alone.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: the custom sampler rack and the type-1 filter path are on main since #4417, so nothing distinct is left here (as you found when rebasing).
Reopen if something here is still missing on current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs needs rebase Conflicts with main, rebase needed prx

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants