Compare commits

..
20 Commits
Author SHA1 Message Date
Roman Tarasov 851c269647 v 1.6beta2 Stabilization of registration
Wait for /dev/lvd1 to be freed to avoid remounting after closing y2bj
The number of game registration attempts has been increased to 2x
The backport is mounted only after successful registration to ensure its successful completion. If you delete a game with a backport, re-registration will only occur after a reboot.
2026-02-27 01:07:49 +03:00
Roman Tarasov c9b3415048 Fixed instant game mounting 2026-02-25 04:05:03 +03:00
Roman Tarasov 11b32a97cf 1.6test2 Restored nullfs mounting 2026-02-23 23:50:10 +03:00
Roman Tarasov a43401391d 1.6test1
Added backport support for images from /data/backports/<TitleID>.
Image mount folder layout has been standardized to /data/imgmnt/<FS>mnt/<filename>
2026-02-23 20:44:07 +03:00
Roman Tarasov 0f7bc24928 Add "force_mount" option 2026-02-23 02:35:29 +03:00
Roman Tarasov 1db534cf82 Update readme 2026-02-23 01:50:05 +03:00
Roman Tarasov 3f81ae589f 1.5beta6 Refactor image mount flow, add per-image RO/RW overrides, and improve mount error reporting
- Rename mount base from /data/ufsmnt to /data/imgmnt
- Add per-image mount mode overrides via config:  image_ro=<filename>, image_rw=<filename>
- Improve mount failure notifications.
- Optimize stale-link recovery path.
2026-02-23 01:16:35 +03:00
Roman Tarasov 32ae0c9c50 Improve startup debug logging flow 2026-02-22 13:35:15 +03:00
Roman Tarasov 5564b4396d v. 1.5beta5 Improve mount lifecycle recovery and link cleanup
Added stale mount.lnk cleanup logic for /user/app/* entries, including broken links and invalid sources.
Added automatic remount recovery: if a previously mounted image becomes inactive while the image file still exists, retry mounting.
Added optional cleanup of related /system_ex/app/<TITLE_ID> nullfs bind mounts when unmounting an image source.
Added debug log rotation on startup
2026-02-22 12:52:49 +03:00
Roman Tarasov abe6bbb92b Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-02-22 02:36:28 +03:00
Roman Tarasov 896e5af0fd Fix RO/RW mount flag mapping and kstuff loader mount-link handling 2026-02-22 02:35:36 +03:00
Roman Tarasov 15776edb8b Update README.md 2026-02-21 12:30:10 +03:00
Roman Tarasov 1bc6477ea7 Fixing exFAT image builds in Windows 2026-02-21 06:16:56 +03:00
Roman Tarasov eb2a4c1036 Fix source stability checks to avoid false wait/skip loops 2026-02-21 05:50:15 +03:00
Roman Tarasov 8f34cabb20 v 1.5beta3
Optimized mounting parameters
Automatic param.json correction
Extended error diagnostics output
Optional recursive folder scanning (slower)
Automatic disk image size calculation in creation scripts
Fixed exFAT image creation on Windows
Expanded documentation, examples, and troubleshooting guidance
2026-02-21 04:40:44 +03:00
Roman Tarasov ce74866768 Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-02-18 23:53:14 +03:00
Roman Tarasov f5421c9c5f Update README.md 2026-02-18 23:52:34 +03:00
Roman Tarasov 1260396ffd Update issue templates 2026-02-18 22:58:58 +03:00
Roman Tarasov 7643b3f7f7 Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-02-18 02:04:18 +03:00
Roman Tarasov c00b162a01 new examples 2026-02-18 02:03:31 +03:00
9 changed files with 1892 additions and 709 deletions

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
---
name: Bug Report
about: Create a report to help us improve ShadowMountPlus
title: "[BUG] Short description of the issue"
labels: bug
assignees: ''
---
**Description**
A clear and concise description of what the bug is.
Please answer the following questions to help us diagnose the issue:
**Software Version**
- **ShadowMountPlus Version:**
- **Path to the game that isn't working:**
- **Image Type:**
- [ ] .fpkg
- [ ] .exfat image
- **How was this image created?**
- **Does it work with any other software?**
- **Firmware Version & Backports:**
**Itemzflow History**
- **Has Itemzflow been used for these games before?**
- [ ] Yes
- [ ] No
- **If YES: Did you delete the game shortcuts using Itemzflow BEFORE installing ShadowMountPlus?**
- [ ] Yes, I deleted them
- [ ] No, I did not delete them
- [ ] N/A
**Debug Log**
Please attach the `debug.log` file. You can drag and drop the file here, or copy and paste the content inside the spoiler block below.
<details>
<summary>Click to expand debug.log content</summary>
```text
PASTE YOUR LOG HERE
```
</details>
+16 -15
View File
@@ -1,21 +1,22 @@
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
# Standard Flags (No extra libraries)
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
# Standard Flags (No extra libraries)
CFLAGS := -O2 -Wall -D_BSD_SOURCE -std=gnu11 -Isrc
# Linker
LDFLAGS :=
# Standard Libraries Only
# Standard Libraries Only
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
# Targets
all: shadowmountplus.elf
# Build Daemon
shadowmountplus.elf: src/main.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
clean:
# Targets
all: shadowmountplus.elf
# Build Daemon
shadowmountplus.elf: src/main.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
clean:
rm -f shadowmountplus.elf kill.elf src/*.o
+115 -64
View File
@@ -3,7 +3,9 @@
PURPOSE
-------
Create a RAW image file, mount it via OSFMount as a logical volume,
auto-select FAT32/exFAT by image size, format it, copy a folder into it, then dismount it.
format it as exFAT, and either:
- format + copy + dismount (default), or
- create + mount only for manual steps.
USAGE (run PowerShell as Administrator)
--------------------------------------
@@ -22,6 +24,13 @@
-Label "DATA" `
-ForceOverwrite
3) Create empty image and keep mounted (manual format/copy):
powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 `
-ImagePath "C:\images\data.exfat" `
-SourceDir "C:\payload\APPXXXX" `
-CreateEmptyAndMount `
-ForceOverwrite
PARAMETERS
----------
-ImagePath Output image file path.
@@ -30,13 +39,16 @@
Suffixes: K/M/G/T (1024), k/m/g/t (1000), b (512-byte blocks), or bytes.
-Label Volume label.
-ForceOverwrite Recreate image if it already exists.
-CreateEmptyAndMount
Create and mount image only. Skip format/copy and leave mounted.
NOTES
-----
- This script does NOT auto-elevate. Start PowerShell as Administrator.
- FS selection is automatic by image size:
* < 4 GB -> FAT32, cluster 4096
* >= 4 GB -> exFAT, cluster 32768
- Filesystem is always exFAT.
- Cluster size is auto-selected:
- large-file sets: 65536
- small/mixed-file sets: 32768
#>
[CmdletBinding()]
@@ -52,7 +64,9 @@ param(
[string]$Label = "OSFIMG",
[switch]$ForceOverwrite
[switch]$ForceOverwrite,
[switch]$CreateEmptyAndMount
)
Set-StrictMode -Version Latest
@@ -74,9 +88,9 @@ function Find-OSFMountCom {
if ($cmd) { return $cmd.Source }
$candidates = @(
"$env:ProgramFiles\OSFMount\osfmount.com",
"${env:ProgramFiles}\OSFMount\osfmount.com",
"${env:ProgramFiles(x86)}\OSFMount\osfmount.com",
"$env:ProgramFiles\PassMark\OSFMount\osfmount.com",
"${env:ProgramFiles}\PassMark\OSFMount\osfmount.com",
"${env:ProgramFiles(x86)}\PassMark\OSFMount\osfmount.com"
) | Where-Object { $_ -and (Test-Path $_) }
@@ -128,39 +142,40 @@ function Get-FreeDriveLetter {
function Get-OptimalImageSizeBytes([string]$dir, [int]$clusterBytes) {
$cluster = [Int64]$clusterBytes
[Int64]$metaFixed = 32MB
[Int64]$minSlack = 64MB
[Int64]$spareMin = 64MB
[Int64]$spareMax = 512MB
[Int64]$entryMetaBytes = 256
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -Force)
$dirs = @(Get-ChildItem -LiteralPath $dir -Recurse -Directory -Force)
$dirs = @(Get-ChildItem -LiteralPath $dir -Recurse -Directory -Force)
[Int64]$sumAllocated = 0
[Int64]$rawFileBytes = 0
[Int64]$dataBytes = 0
foreach ($f in $files) {
$len = [Int64]$f.Length
$alloc = (($len + $cluster - 1) / $cluster) * $cluster
$sumAllocated += $alloc
$rawFileBytes += $len
$dataBytes += [Int64]([Math]::Ceiling($len / [double]$cluster) * $cluster)
}
# Metadata estimate (moderate, not bloated)
[Int64]$metaEntries = ([Int64]$files.Count * 512) + ([Int64]$dirs.Count * 256)
[Int64]$dataClusters = [Int64]([Math]::Ceiling($dataBytes / [double]$cluster))
[Int64]$fatBytes = $dataClusters * 4
[Int64]$bitmapBytes = [Int64]([Math]::Ceiling($dataClusters / 8.0))
[Int64]$entryBytes =
(([Int64]$files.Count + [Int64]$dirs.Count) * $entryMetaBytes)
[Int64]$clustersData = [Int64]([Math]::Ceiling($sumAllocated / [double]$cluster))
[Int64]$bitmap = [Int64]([Math]::Ceiling($clustersData / 8.0)) # 1 bit per cluster
[Int64]$fat = $clustersData * 4 # rough 4 bytes per cluster
[Int64]$baseTotal =
$dataBytes + $fatBytes + $bitmapBytes + $entryBytes + $metaFixed
[Int64]$spareBytes = [Int64]([Math]::Ceiling($baseTotal / 200.0))
if ($spareBytes -lt $spareMin) { $spareBytes = $spareMin }
if ($spareBytes -gt $spareMax) { $spareBytes = $spareMax }
[Int64]$total = $baseTotal + $spareBytes
[Int64]$minTotal = $rawFileBytes + $minSlack
if ($total -lt $minTotal) { $total = $minTotal }
[Int64]$baseline = 16MB
[Int64]$raw = $sumAllocated + $metaEntries + $bitmap + $fat + $baseline
# Safety margin: 2% capped to 512 MiB
[Int64]$margin = [Int64]([Math]::Ceiling($raw * 0.02))
if ($margin -gt 512MB) { $margin = 512MB }
if ($margin -lt 8MB) { $margin = 8MB }
[Int64]$total = $raw + $margin
if ($total -lt 64MB) { $total = 64MB }
# Align to 1 MiB
[Int64]$align = 1MB
$total = (($total + $align - 1) / $align) * $align
$total = [Int64]([Math]::Ceiling($total / [double]$align) * $align)
return $total
}
@@ -182,6 +197,32 @@ function Get-LogicalDriveFileSystem([string]$driveLetter) {
return ""
}
function Get-OptimalExfatClusterSize([string]$dir) {
[Int64]$largeFileThreshold = 1MB
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -Force)
if ($files.Count -eq 0) { return 32768 }
[Int64]$rawFileBytes = 0
foreach ($f in $files) {
$rawFileBytes += [Int64]$f.Length
}
[Int64]$avgFileBytes = [Int64]($rawFileBytes / [Int64]$files.Count)
if ($avgFileBytes -ge $largeFileThreshold) { return 65536 }
return 32768
}
function Format-AllocationUnitArg([int]$clusterSize) {
if ($clusterSize -ge 1MB -and ($clusterSize % 1MB) -eq 0) {
return "{0}M" -f ($clusterSize / 1MB)
}
if ($clusterSize -ge 1KB -and ($clusterSize % 1KB) -eq 0) {
return "{0}K" -f ($clusterSize / 1KB)
}
return "$clusterSize"
}
function Dismount-OsfVolume([string]$osfPath, [string]$mountPoint, [int]$maxAttempts = 6) {
if ([string]::IsNullOrWhiteSpace($mountPoint)) { return $false }
@@ -203,14 +244,12 @@ function Dismount-OsfVolume([string]$osfPath, [string]$mountPoint, [int]$maxAtte
return $false
}
function Invoke-FormatVolume([string]$driveLetter, [string]$fileSystem, [int]$clusterSize, [string]$label) {
function Invoke-FormatVolume([string]$driveLetter, [int]$clusterSize, [string]$label) {
$target = "${driveLetter}:"
if ($fileSystem -notin @("FAT32", "exFAT")) {
throw "Unsupported file system '$fileSystem'. Expected FAT32 or exFAT."
}
$fileSystem = "exFAT"
$clusterArg = Format-AllocationUnitArg -clusterSize $clusterSize
$attempts = @(
@{ Name = "$fileSystem quick with requested allocation unit"; Args = @($target, "/FS:$fileSystem", "/A:$clusterSize", "/Q", "/V:$label", "/X", "/Y") }
@{ Name = "$fileSystem quick with requested allocation unit"; Args = @($target, "/FS:$fileSystem", "/A:$clusterArg", "/Q", "/V:$label", "/X", "/Y") }
)
$lastFormatExitCode = -1
@@ -222,6 +261,14 @@ function Invoke-FormatVolume([string]$driveLetter, [string]$fileSystem, [int]$cl
$proc = Start-Process -FilePath "format.com" -ArgumentList $attempt.Args -Wait -PassThru -NoNewWindow `
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath
$lastFormatExitCode = [int]$proc.ExitCode
# Show native format.com output in current console.
if (Test-Path -LiteralPath $stdoutPath) {
Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
}
if (Test-Path -LiteralPath $stderrPath) {
Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
}
} finally {
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
}
@@ -246,6 +293,7 @@ function Invoke-FormatVolume([string]$driveLetter, [string]$fileSystem, [int]$cl
if (-not (Test-Admin)) { throw "Please run PowerShell as Administrator." }
if (-not (Test-Path $SourceDir -PathType Container)) { throw "Source directory not found: $SourceDir" }
if (-not (Test-Path (Join-Path $SourceDir "eboot.bin") -PathType Leaf)) { throw "eboot.bin not found in source directory: $SourceDir" }
# Ensure output directory exists
$outDir = Split-Path -Parent $ImagePath
@@ -260,48 +308,41 @@ if (Test-Path $ImagePath) {
[Int64]$expectedBytes = 0
[string]$osfSizeArg = $null
[Int64]$FsSwitchThresholdBytes = 4GB
[int]$Fat32ClusterSize = 4096
[int]$ExfatClusterSize = 32768
[string]$TargetFs = ""
[int]$TargetClusterSize = 0
[int]$ExfatClusterSize = Get-OptimalExfatClusterSize -dir $SourceDir
[bool]$sizeProvided = -not [string]::IsNullOrWhiteSpace($Size)
[int]$TargetClusterSize = $ExfatClusterSize
if ([string]::IsNullOrWhiteSpace($Size)) {
if (-not $sizeProvided) {
Write-Host "[Info] Size not provided. Computing an optimal image size from '$SourceDir'..."
$fat32Candidate = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $Fat32ClusterSize
if ($fat32Candidate -lt $FsSwitchThresholdBytes) {
$expectedBytes = $fat32Candidate
} else {
$expectedBytes = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $ExfatClusterSize
}
$expectedBytes = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $TargetClusterSize
$osfSizeArg = "$expectedBytes"
Write-Host "[Info] Computed image size: $(Format-Bytes $expectedBytes) ($expectedBytes bytes)."
} else {
$expectedBytes = Parse-SizeToBytes $Size
$osfSizeArg = $Size
}
if ($expectedBytes -lt $FsSwitchThresholdBytes) {
$TargetFs = "FAT32"
$TargetClusterSize = $Fat32ClusterSize
} else {
$TargetFs = "exFAT"
$TargetClusterSize = $ExfatClusterSize
if (-not $sizeProvided) {
Write-Host "[Info] Computed image size: $(Format-Bytes $expectedBytes) ($expectedBytes bytes)."
}
Write-Host "[Info] Selected filesystem: $TargetFs (cluster=$TargetClusterSize) for image size $(Format-Bytes $expectedBytes)."
Write-Host "[Info] Selected filesystem: exFAT (cluster=$TargetClusterSize) for image size $(Format-Bytes $expectedBytes)."
$osf = Find-OSFMountCom
[string]$DriveLetter = ""
[string]$MountPoint = ""
[bool]$Mounted = $false
[bool]$LeaveMounted = $CreateEmptyAndMount.IsPresent
try {
$DriveLetter = Get-FreeDriveLetter
$MountPoint = "${DriveLetter}:"
Write-Host "[1/4] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
$out = & $osf -a -t file -f $ImagePath -s $osfSizeArg -m $MountPoint -o rw 2>&1
if ($CreateEmptyAndMount) {
Write-Host "[1/2] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
} else {
Write-Host "[1/4] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
}
$out = & $osf -a -t file -f $ImagePath -s $osfSizeArg -m $MountPoint -o rw,rem 2>&1
Write-Host ($out | Out-String).Trim()
if ($LASTEXITCODE -ne 0) { throw "osfmount.com failed with exit code $LASTEXITCODE." }
$Mounted = $true
@@ -310,26 +351,36 @@ try {
}
$dest = "${DriveLetter}:\"
Write-Host "[2/4] Formatting $dest as $TargetFs (cluster=$TargetClusterSize, label='$Label') via format.com ..."
Invoke-FormatVolume -driveLetter $DriveLetter -fileSystem $TargetFs -clusterSize $TargetClusterSize -label $Label
if ($CreateEmptyAndMount) {
Write-Host "[2/2] Done. Empty image is mounted at $dest."
Write-Host "Manual steps:"
Write-Host " 1) Format $dest as exFAT (recommended cluster: 64KB for large-file sets, 32KB for small/mixed sets)."
Write-Host " 2) Copy contents of '$SourceDir' to $dest."
Write-Host " 3) Dismount: `"$osf`" -d -m $MountPoint"
return
}
Write-Host "[2/4] Formatting $dest as exFAT (cluster=$TargetClusterSize, label='$Label') via format.com ..."
Invoke-FormatVolume -driveLetter $DriveLetter -clusterSize $TargetClusterSize -label $Label
if (-not (Test-Path $dest)) { throw "Drive $dest is not accessible after formatting." }
Write-Host "[3/4] Copying '$SourceDir' -> '$dest' ..."
Write-Host "[3/4] Copying contents of '$SourceDir' -> '$dest' ..."
$roboArgs = @(
$SourceDir, $dest,
"/E", "/COPY:DAT", "/DCOPY:DAT",
"/R:1", "/W:1",
"/NFL", "/NDL", "/NP", "/NJH", "/NJS"
"/NFL", "/NDL",
"/ETA"
)
& robocopy.exe @roboArgs | Out-Null
& robocopy.exe @roboArgs
$robocopyExitCode = $LASTEXITCODE
if ($robocopyExitCode -gt 7) { throw "robocopy failed. Exit code: $robocopyExitCode" }
Write-Host "[4/4] Done. Dismounting OSFMount volume..."
}
finally {
if ($Mounted -and -not [string]::IsNullOrWhiteSpace($MountPoint)) {
if ($Mounted -and -not $LeaveMounted -and -not [string]::IsNullOrWhiteSpace($MountPoint)) {
try {
$currentPath = (Get-Location).Path
if ($currentPath -like "$MountPoint*") {
+122 -39
View File
@@ -1,6 +1,10 @@
# ShadowMountPlus (PS5)
**Version:** `1.5beta2`
**Version:** `1.6beta1`
**Repository:** https://github.com/drakmor/shadowMountPlus
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
@@ -15,42 +19,67 @@
| Extension | Mounted FS | Attach backend | Status |
| --- | --- | --- | --- |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Stable |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | High performance |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Optimal |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Legacy |
| `.ffpfs` | `pfs` | `LVD` | Experimental |
Notes:
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
- Optional debug logging can be enabled via `debug=1` (writes to console and `/data/shadowmount/debug.log`).
- Debug logging is enabled by default (`debug=1`) and writes to console plus `/data/shadowmount/debug.log` (set `debug=0` to disable).
- **exFAT is the preferred image filesystem, including on 4.xx firmware, with no known reboot/shutdown issues in typical use.**
## Recommended FS choice
- Prefer **UFS (`.ffpkg`)** for games that work correctly on UFS: it is generally more performant (~1.5-2.5 x exFAT).
- Use **exFAT (`.exfat`)** for games that only work from external-disk style layouts, because exFAT is case-insensitive.
- Prefer **exFAT (`.exfat`)** in most cases: it is generally more performant, case-insensitive, and does not have reboot/shutdown issues in typical use.
- Use **UFS (`.ffpkg`)** only when specifically needed for compatibility with your game/setup.
## Runtime config (`/data/shadowmount/config.ini`)
This file is optional. If it does not exist, built-in defaults are used.
Supported keys (all optional):
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `0`)
- `mount_read_only=1|0`
- `exfat_backend=lvd|md`
- `ufs_backend=lvd|md`
- `scanpath=<absolute_path>` (can be repeated on multiple lines)
- `exfat_sector_size=<value>`
- `ufs_sector_size=<value>`
- `pfs_sector_size=<value>`
- `lvd_exfat_sector_size=<value>`
- `lvd_ufs_sector_size=<value>`
- `lvd_pfs_sector_size=<value>`
- `md_exfat_sector_size=<value>`
- `md_ufs_sector_size=<value>`
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
- `mount_read_only=1|0` (default: `1`)
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
- `recursive_scan=1|0` (`0` = scan only first-level subfolders, `1` = recursive scan without depth limit; default: `0`)
- `backports_path=<absolute_path>` (default: `/data/backports`; if `<backports_path>/<TITLE_ID>` exists, it is overlaid on mounted image via `unionfs`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `10`)
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
- `exfat_backend=lvd|md` (default: `lvd`)
- `ufs_backend=lvd|md` (default: `lvd`)
- `scanpath=<absolute_path>` (can be repeated on multiple lines; default: built-in scan path list below)
- `lvd_exfat_sector_size=<value>` (default: `512`)
- `lvd_ufs_sector_size=<value>` (default: `4096`)
- `lvd_pfs_sector_size=<value>` (default: `32768`)
- `md_exfat_sector_size=<value>` (default: `512`)
- `md_ufs_sector_size=<value>` (default: `512`)
Per-image mode override behavior:
- Match is done by image file name (without path).
- File names with spaces are supported.
- If multiple rules target the same file name, the last one in config wins.
- If no rule matches, global `mount_read_only` is used.
- Example:
```ini
mount_read_only=1
image_rw=PPSA1234-my-image.ffpfs
image_rw=MYGame 123.exfat
image_ro=legacy_dump.ffpkg
```
Scan path behavior:
- If at least one `scanpath=...` is present, only those custom paths are used.
- `/data/ufsmnt` is always added automatically, even with custom paths.
- Full scan loop runs every 10 seconds.
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always added automatically, even with custom paths.
- With `recursive_scan=0` (default), only first-level subfolders are checked.
- With `recursive_scan=1`, subfolders are scanned recursively.
- Full scan loop runs every `scan_interval_seconds` (default: `10`).
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
Backport overlay behavior:
- If `${backports_path}/${TITLE_ID}` exists, ShadowMount+ mounts it over the game root with `unionfs`.
- Files from backport folder override files from the mounted image.
Validation:
- See `config.ini.example` for a ready-to-use template.
@@ -59,7 +88,15 @@ Validation:
Image mountpoints are created under:
`/data/ufsmnt/<image_name>-<fs_suffix>`
`/data/imgmnt/<fs>/<image_name>`
Where `<fs>` is one of: `ufsmnt`, `exfatmnt`, `pfsmnt`.
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
- Game files must be placed at the image root.
- Do not add an extra top-level folder inside the image.
- Valid example: `/sce_sys/param.json` exists directly from image root.
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
## Scan paths
@@ -75,21 +112,36 @@ Default scan locations:
- `/mnt/usb0` .. `/mnt/usb7`
- `/mnt/ext0`
- `/mnt/ext1`
- `/data/ufsmnt` (mounted image content scan)
- `/data/imgmnt/ufsmnt`, `/data/imgmnt/exfatmnt`, `/data/imgmnt/pfsmnt` (mounted image content scan)
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
Recommended folder structure:
- Default mode (`recursive_scan=0`):
- `/data/homebrew/<TITLE_ID>/`
- `/data/etaHEN/games/<TITLE_ID>/`
- Recursive mode (`recursive_scan=1`):
- `/data/homebrew/PS5/<AnyFolder>/<TITLE_ID>/`
- `/mnt/ext0/etaHEN/games/<Collection>/<TITLE_ID>/`
## Creating an exFAT image
Linux (Ubuntu/Debian):
- `sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
- `truncate -s <image_size> test.exfat`
- `mkfs.exfat -c 32768 test.exfat`
- `mkdir -p /mnt/exfat`
- `mount -t exfat-fuse -o loop test.exfat /mnt/exfat`
- `rsync -r --info=progress2 APPXXXX/ /mnt/exfat/`
- `umount /mnt/exfat`
- Required components installation:
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
- Script: `mkexfat.sh`
- Usage: `./mkexfat.sh <game_root_dir> [output_file]`
- Example:
- `chmod +x mkexfat.sh`
- `./mkexfat.sh ./APPXXXX ./PPSA12345.exfat`
- Notes:
- Source folder must be the game root and contain `eboot.bin`.
- Auto-calculates image size using rounded file allocation + metadata + safety margin.
- Automatically selects exFAT cluster profile:
- Large-file profile: `64K`
- Small/mixed-file profile: `32K`
Windows:
- Recommended: use `make_image.bat` (wrapper for `New-OsfExfatImage.ps1` + OSFMount).
@@ -101,8 +153,8 @@ Windows:
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
- Behavior:
- Auto-sizes the image to fit source content.
- Formats and copies the source folder into the image.
- Overwrites existing image file (uses `-ForceOverwrite`).
- Source folder must be the game root and contain `eboot.bin`.
- Formats and copies source folder contents into image root.
- Optional (fixed size): run PowerShell script directly:
- `powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 -ImagePath "C:\images\game.exfat" -SourceDir "C:\payload\APPXXXX" -Size 8G -ForceOverwrite`
@@ -110,18 +162,23 @@ Windows:
FreeBSD:
- Script: `mkufs2.sh`
- Usage: `./mkufs2.sh <input_dir> [output_file]`
- Usage: `./mkufs2.sh <game_root_dir> [output_file]`
- Example:
- `chmod +x mkufs2.sh`
- `./mkufs2.sh ./APPXXXX ./PPSA12345.ffpkg`
- Notes:
- The script auto-calculates image size (`source + 20% + 10MB` slack).
- Uses UFS2 format (`newfs -O 2`) with `-b 32768 -f 4096`.
- Requires BSD tools like `mdconfig`, `newfs`, `mount`, `umount`.
- Source folder must be the game root and contain `eboot.bin`.
- The script auto-calculates image size using rounded file allocation + metadata + safety margin.
- Recommended `newfs` parameters for UFS2:
- Large-file profile: `newfs -O 2 -b 65536 -f 4096 -m 0 -i 262144`
- Small/mixed-file profile: `newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144`
- `mkufs2.sh` selects between these two profiles automatically (based on average file size).
Windows:
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
- Recommended output format for this project is `.ffpkg`.
- Example:
- `UFS2Tool.exe newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
## Installation and usage
@@ -140,15 +197,41 @@ Add ShadowMountPlus to your `autoload.txt` for **plk-autoloader** to ensure it s
**Sample Configuration:**
```ini
!1000
kstuff.elf
!1000
notify.elf ; Optional - Remove this line if you do not want Rich Toasts
!1000
shadowmountplus.elf
!3000
ps5-backpork.elf
!1000
kstuff.elf
```
---
## Troubleshooting
If a game is not mounted:
- Debug log is enabled by default; if disabled, set `debug=1` in `/data/shadowmount/config.ini`.
- Check `/data/shadowmount/debug.log` and system notifications from ShadowMount+.
- Verify scan roots:
- if `scanpath=...` is set, only these paths are scanned;
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always scanned.
- Verify scan depth:
- `recursive_scan=0` scans only first-level subfolders;
- `recursive_scan=1` scans recursively.
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
- Verify game structure:
- folder game: `<GAME_DIR>/sce_sys/param.json`;
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/data/imgmnt/<fs>/<image_name>/` and include `sce_sys/param.json`.
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS).
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
If a game is mounted but does not start:
- Check registration notifications (`Register failed ...`).
- If the game is not registered, try removing its launcher icon and removing it from Itemzflow.
- If this does not help, remove the game data from system settings and retry (this will delete game saves).
## ⚠️ Notes
* **First Run:** If you have a large library, the initial scan may take a few seconds to register all titles.
* **Large Games:** For massive games (100GB+), allow a few extra seconds for the system to verify file integrity before the "Installed" notification appears.
+37 -9
View File
@@ -4,26 +4,49 @@
# All keys are optional.
# If a key is missing, built-in defaults are used.
# Debug logging:
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
# 0/false/no/off -> disable log_debug output
debug=1
# Global mount mode:
# 1/true/yes/on/ro -> read-only mounts
# 0/false/no/off/rw -> read-write mounts
mount_read_only=1
# Mounting even damaged file systems
# 1/true/yes/on -> include force option
# 0/false/no/off -> default mount options (default)
force_mount=0
# Per-image mount mode override by file name (can be repeated many times):
# image_ro=<image_filename>
# image_rw=<image_filename>
# image_ro=PPSA12345.ffpfs
# image_rw=My Game Dump.exfat
# Recursive scan mode:
# 1/true/yes/on -> scan subdirectories recursively (no depth limit)
# 0/false/no/off -> scan only first-level subdirectories (default)
recursive_scan=0
# Optional backport overlay root:
# If "<backports_path>/<TITLE_ID>" exists, it is mounted over image root via unionfs.
backports_path=/data/backports
# Full scan loop interval (seconds), range: 1..3600
scan_interval_seconds=10
# Source stabilization wait window (seconds), range: 0..3600
# If source mtime is newer than this window, it will be deferred to next scan.
stability_wait_seconds=10
# Backend selection per filesystem:
# lvd -> /dev/lvdctl -> /dev/lvdN
# md -> /dev/mdctl -> /dev/mdN
exfat_backend=lvd
ufs_backend=lvd
# Common sector-size shortcuts (apply to both LVD and MD where applicable):
# exfat_sector_size -> lvd_exfat_sector_size + md_exfat_sector_size
# ufs_sector_size -> lvd_ufs_sector_size + md_ufs_sector_size
# pfs_sector_size -> lvd_pfs_sector_size
#
exfat_sector_size=512
ufs_sector_size=4096
pfs_sector_size=32768
# Backend-specific overrides (optional):
# lvd_exfat_sector_size=512
# lvd_ufs_sector_size=4096
@@ -31,3 +54,8 @@ pfs_sector_size=32768
# md_exfat_sector_size=512
# md_ufs_sector_size=512
# Optional custom scan roots (can be repeated):
# If at least one scanpath=... is present, only these paths are used.
# "/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}" are always added automatically.
# scanpath=/data/homebrew
# scanpath=/data/etaHEN/games
+6 -4
View File
@@ -24,13 +24,15 @@ if not exist "%SRCDIR%" (
exit /b 3
)
if not exist "%SRCDIR%\eboot.bin" (
echo [ERROR] eboot.bin not found in source directory: "%SRCDIR%"
exit /b 4
)
REM Run elevated? This BAT does not auto-elevate.
REM Right-click -> Run as administrator, or start cmd as admin.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" ^
-ImagePath "%IMAGE%" ^
-SourceDir "%SRCDIR%" ^
-ForceOverwrite
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" -ImagePath "%IMAGE%" -SourceDir "%SRCDIR%" -ForceOverwrite
set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" (
+71 -11
View File
@@ -1,6 +1,6 @@
#!/bin/sh
# For Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
# Create a exFAT image from a directory
# For WSL2/Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
# Create an exFAT image from a directory
# Usage: mkexfat.sh <input_dir> [output_file]
set -e
@@ -11,25 +11,85 @@ if [ -z "$1" ]; then
fi
INPUT_DIR="$1"
OUTPUT="${2:-test.ffpkg}"
OUTPUT="${2:-test.exfat}"
# Calculate required size (dir size + 20% overhead + 10MB slack)
BYTES=$(du -s -k "$INPUT_DIR" | awk '{print $1 * 1024}')
OVERHEAD=$((BYTES / 5)) # 20%
SLACK=$((10 * 1024 * 1024)) # 10MB
TOTAL=$((BYTES + OVERHEAD + SLACK))
if [ ! -d "$INPUT_DIR" ]; then
echo "Error: input directory not found: $INPUT_DIR"
exit 1
fi
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
exit 1
fi
# More accurate sizing for exFAT:
# - file payload rounded to cluster size
# - FAT + allocation bitmap estimates from cluster count
# - directory/entry metadata estimate
# - fixed metadata and runtime headroom
CLUSTER_SIZE=32768
MKFS_CLUSTER_ARG="32K"
LARGE_FILE_THRESHOLD=$((1024 * 1024))
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
ENTRY_META_BYTES=256
FILE_COUNT=$(find "$INPUT_DIR" -type f | wc -l | tr -d ' ')
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
awk '{s += $1} END {print s + 0}')
AVG_FILE_BYTES=0
if [ "$FILE_COUNT" -gt 0 ]; then
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
fi
# exFAT profile selection (same idea as UFS2 profile):
# - large-file sets: 64K cluster
# - small/mixed-file sets: 32K cluster
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
CLUSTER_SIZE=65536
MKFS_CLUSTER_ARG="64K"
fi
DATA_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
awk -v cls="$CLUSTER_SIZE" '{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}')
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
FAT_BYTES=$((DATA_CLUSTERS * 4))
BITMAP_BYTES=$(( (DATA_CLUSTERS + 7) / 8 ))
ENTRY_BYTES=$(( (FILE_COUNT + DIR_COUNT) * ENTRY_META_BYTES ))
BASE_TOTAL=$((DATA_BYTES + FAT_BYTES + BITMAP_BYTES + ENTRY_BYTES + META_FIXED))
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then
SPARE_BYTES=$SPARE_MIN
fi
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then
SPARE_BYTES=$SPARE_MAX
fi
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then
TOTAL=$MIN_TOTAL
fi
# Round up to nearest MB
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
echo "Input size: $BYTES bytes"
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "exFAT profile: -c $MKFS_CLUSTER_ARG (avg file=$AVG_FILE_BYTES bytes)"
echo "Image size: ${MB}MB"
truncate -s "${MB}M" "$OUTPUT"
mkfs.exfat -c 4K "$OUTPUT"
mkfs.exfat -c "$MKFS_CLUSTER_ARG" "$OUTPUT"
mkdir -p /mnt/exfat
mount -t exfat-fuse -o loop "$OUTPUT" /mnt/exfat
rsync -r --info=progress2 "$INPUT_DIR" /mnt/exfat/
rsync -r --info=progress2 "$INPUT_DIR"/ /mnt/exfat/
umount /mnt/exfat
+67 -7
View File
@@ -12,22 +12,82 @@ fi
INPUT_DIR="$1"
OUTPUT="${2:-download0.dat}"
# Calculate required size (dir size + 20% overhead + 10MB slack)
BYTES=$(du -s -k "$INPUT_DIR" | awk '{print $1 * 1024}')
OVERHEAD=$((BYTES / 5)) # 20%
SLACK=$((10 * 1024 * 1024)) # 10MB
TOTAL=$((BYTES + OVERHEAD + SLACK))
if [ ! -d "$INPUT_DIR" ]; then
echo "Error: input directory not found: $INPUT_DIR"
exit 1
fi
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
exit 1
fi
# More accurate sizing for UFS2:
# - file payload rounded to fragment size (-f)
# - minimal per-directory allocation
# - inode table estimate
# - fixed filesystem metadata headroom
BLOCK_SIZE=32768
FRAG_SIZE=4096
MINFREE_PERCENT=0
BYTES_PER_INODE=262144
INODE_SIZE=256
INODE_SPARE=2048
META_FIXED=$((64 * 1024 * 1024)) # superblocks, cg metadata, allocator slack
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
FILE_COUNT=$(find "$INPUT_DIR" -type f | wc -l | tr -d ' ')
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
awk '{s += $1} END {print s + 0}')
AVG_FILE_BYTES=0
if [ "$FILE_COUNT" -gt 0 ]; then
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
fi
# UFS profile selection:
# - large-file sets: 64K block
# - small/mixed-file sets: 32K block (safer/denser default)
if [ "$AVG_FILE_BYTES" -ge $((1024 * 1024)) ]; then
BLOCK_SIZE=65536
fi
DATA_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
awk -v frag="$FRAG_SIZE" '{s += int(($1 + frag - 1) / frag) * frag} END {print s + 0}')
DIR_BYTES=$((DIR_COUNT * FRAG_SIZE))
INODE_COUNT=$((FILE_COUNT + DIR_COUNT + INODE_SPARE))
INODE_BYTES=$((INODE_COUNT * INODE_SIZE))
BASE_TOTAL=$((DATA_BYTES + DIR_BYTES + INODE_BYTES + META_FIXED))
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then
SPARE_BYTES=$SPARE_MIN
fi
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then
SPARE_BYTES=$SPARE_MAX
fi
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then
TOTAL=$MIN_TOTAL
fi
# Round up to nearest MB
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
echo "Input size: $BYTES bytes"
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
echo "Input size (UFS alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -i $BYTES_PER_INODE (avg file=$AVG_FILE_BYTES bytes)"
echo "Image size: ${MB}MB"
truncate -s "${MB}M" "$OUTPUT"
MD=$(mdconfig -a -t vnode -f "$(realpath "$OUTPUT")")
newfs -O 2 -b 32768 -f 4096 /dev/${MD}
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -i "$BYTES_PER_INODE" /dev/${MD}
mkdir -p /mnt
mount /dev/${MD} /mnt
+1405 -560
View File
File diff suppressed because it is too large. Load diff