Skip to content

Faster unpack compression backend - #62

Merged
RenanGBarreto merged 4 commits into
PSBrew:mainfrom
rdmrocha:enh/perf
Jun 23, 2026
Merged

RenanGBarreto merged 4 commits into
PSBrew:mainfrom
rdmrocha:enh/perf

Conversation

@rdmrocha

Copy link
Copy Markdown
Contributor

Two performance changes on the IO/compression paths.

Unpack previously decoded every payload twice (the upfront integrity pass, then again while writing) and reported progress once per file. Now extract_pfs_image calls inspect_pfs_image(verify_payloads=False), decoding each payload exactly once while writing, with byte-based throttled progress.

  • ~2× less decode work when unpacking compressed images.
  • Constant memory — streams block-by-block instead of buffering whole files (~30 MB peak on a 1 GiB logical image).
  • Extraction progress moves incrementally to 100% instead of jumping at the end.

Compression backend switches from stdlib zlib to zlib-ng (drop-in: same compress/decompress/crc32, RFC-1950 compatible).

  • ~1.5–1.7× faster compression at equal-or-better ratio (benchmarked per 64 KB PFSC block on ~50%-compressible data).
  • Backward compatible: existing images still decode; new images read by any zlib consumer.
  • Adds zlib-ng as a runtime dependency.

Output is otherwise unchanged (PFS/PFSC format identical). Verified with round-trip pack→unpack, a stdlib-zlib-decode-compat test, and the existing suite.

Abdo-ka added a commit to Abdo-ka/MkPFS that referenced this pull request Jun 21, 2026
Adds a --no-progress flag to the unpack command so the extraction
progress bar can be silenced for non-interactive use (scripts, CI,
piping output to a file). Complements the progress reporting added in
PR PSBrew#62 by making the reporter's enabled state respect the flag.

Includes a regression test for the disabled path and documents the
flag in the README unpack section.

@RenanGBarreto RenanGBarreto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you.
LGTM, let me test. and then I will merge

@RenanGBarreto RenanGBarreto self-assigned this Jun 21, 2026
@RenanGBarreto RenanGBarreto added the investigation needed We would need to investigate. label Jun 21, 2026
@RenanGBarreto
RenanGBarreto merged commit 9653d7c into PSBrew:main Jun 23, 2026
3 checks passed
@RenanGBarreto RenanGBarreto changed the title perf: faster unpack and zlib-ng compression backend Faster unpack compression backend Jun 23, 2026
RenanGBarreto added a commit that referenced this pull request Jun 27, 2026
…folder, deep read (#65)

## Why

Producing an exFAT-wrapped game image today depends on the host OS and
elevated privileges: it shells out to `newfs_exfat`/`mkfs.exfat`, mounts
a loopback/disk image (`losetup`/`hdiutil`/OSFMount), copies files in as
root/admin, and unmounts.

That means three divergent code paths, external tools that must be
installed, and admin rights — and it cannot run unprivileged or on a
machine without the right filesystem tooling. It also forces a two-step
workflow (build a full `.exfat`, then compress it), so peak disk is
roughly double the game size while both the intermediate image and the
output exist.

This PR removes all of that. exFAT is built and read entirely in pure
Python — no mount, no root, no external binaries — identically on macOS,
Linux, and Windows, and the folder→`.ffpfsc` path runs in a single
streaming pass with no temporary `.exfat` on disk.
`mkpfs pack folder ./game ./game.ffpfsc` now wraps the folder in an
exFAT and compresses it straight into the `.ffpfsc` in one pass.

## New commands and flags

- `pack folder` (default): folder → exFAT → `.ffpfsc` in one pass, no
temp image.
- `pack folder --raw`: pack the folder directly as PFS (the previous
default).
- `pack exfat <dir> [out]`: build a raw `.exfat` image (auto-named
`<titleId>.exfat`).
- `unpack --deep`: extract the files *inside* a wrapped exFAT, not the
inner `.exfat`.
- `tree --deep`: list the files inside a wrapped exFAT. 

## How it works

- **Forward-only exFAT serializer**: computes the full layout up front
(sizes are known after scanning) and emits the image in strict offset
order, so the byte stream feeds straight into the compressor — no
intermediate file, flat memory.
- **Read-only exFAT parser** over any seekable source, plus a
PFSC-backed view so a `.ffpfsc` can be peeled to its inner contents
without writing a temp `.exfat`.
- **Multicore compression** on the fused path (bounded look-ahead thread
pool; zlib-ng releases the GIL), deterministic output.
- Inner image is named `<titleId>.exfat` from `sce_sys/param.json`.
- OS-generated metadata (`.DS_Store`, `._*`, `__MACOSX`, `Thumbs.db`, …)
is excluded consistently across packing, the AMPR index, and verify.
  
## Disk and speed

- Peak disk drops to `source + output` (no inner `.exfat`, no doubling).
- Fused compression scales across cores (≈6x vs single-threaded in local
tests), byte-identical regardless of worker count.
  
## Validation

- The generated exFAT is accepted by a real PS5 (boot-tested via
ShadowMountPlus), is `fsck_exfat`-clean, and mounts/reads correctly
under the macOS kernel driver.
- Round-trip tested through an independent in-tree exFAT reader;
committed tests use a tiny gzipped fixture (no mount, portable). Full
suite green.
  
## Notes

- The fused path is unsigned only; use `--raw` for signed PFS images.

## Dependencies

This branch is stacked on two PRs and should be merged after both:

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

Labels

investigation needed We would need to investigate.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants