.pak archives via the pak:// URI scheme.
pak.manifest.json declaring schemaVersion, plus arbitrary entries (glXF, glTF, textures, audio, splat, etc.). Apps download a .pak to writable storage themselves (e.g. via NetworkedAssetLoader or any HTTP client) and then call PakManager.mount to make its contents addressable as pak://<name>/<path>.
pak:// value (Audio, Mesh, GLXF, particles, etc.) loads transparently from the pak. The lower-level choke points in C++ (androidFOpen / the native asset reader) and in Kotlin (PakManager.openInputStream) handle scheme dispatch — so call sites that already accept apk:// work for pak:// without further code.
PakManager.mount("content", File(context.filesDir, "content.pak"))
glxfManager.inflateGLXF(
Uri.parse("pak://content/scenes/Composition.glxf"),
rootEntity, "main")
.pak file can be mounted under different names — useful when an authored glXF references children via the #qualify shorthand.
@DoNotStripNative — apps that never reference any PakManager.* symbol (e.g. media-only apps that only ever load apk:// and file://) let R8 strip the entire class, the EntryRegion holder, and every external fun from their DEX. The native side (registerPakManager in JavaAPIs_Pak.cpp) is defensive: if FindClass fails because R8 stripped the class, registration silently no-ops instead of crashing JNI_OnLoad.
object PakManager
exists
(
uri
)
|
Returns true if PakManager.exists resolves to a real entry in a mounted pak.
Signature
fun exists(uri: String): Boolean Parameters uri: StringReturns Boolean |
exists
(
context
, uri
)
|
Existence check that mirrors PakManager.openInputStream's scheme dispatch.
Signature
fun exists(context: Context, uri: Uri): Boolean Parameters context: Contexturi: UriReturns Boolean |
getEntryRegion
(
uri
)
|
Returns the on-disk location of an entry, or null if the URI is malformed, the mount is unknown, or the entry is missing. O(1) hash lookup — no I/O.
Signature
fun getEntryRegion(uri: String): PakManager.EntryRegion? Parameters uri: StringReturns PakManager.EntryRegion? |
hasSignatureKey
()
|
Returns true if a non-empty signature key is currently registered.
Signature
fun hasSignatureKey(): Boolean Returns Boolean |
isMounted
(
name
)
|
Signature
fun isMounted(name: String): Boolean Parameters name: StringReturns Boolean |
mapWhole
(
uri
)
|
Returns a read-only MappedByteBuffer over the entry's bytes via FileChannel.map(). Returns null for DEFLATE entries (no contiguous mmap region exists), missing entries, or unknown mounts — callers fall back to PakManager.readWhole in those cases.
The returned buffer's lifetime is owned by the JVM: when it is GC'd, FileChannel's internal cleaner unmaps the region. No app-side Cleaner is needed (this works on minSdk 21 unlike java.lang.ref.Cleaner which requires API 24).
Hot consumers (200+ MiB splats, multi-MiB OGG audio, KTX/Basis textures) should read directly from this buffer — for splat in particular, this collapses three allocations (native vector, JVM ByteArray, native pinned copy) into zero on the load path.
Signature
fun mapWhole(uri: String): MappedByteBuffer? Parameters uri: StringReturns MappedByteBuffer? |
mount
(
name
, absPath
)
|
Mounts the .pak file at PakManager.mount under PakManager.mount. The file is opened read-only and its central directory is indexed once. The native runtime keeps the file open until PakManager.unmount is called; in-flight reads are lifetime-safe across unmount.
Signature
fun mount(name: String, absPath: String): PakManager.MountStatus Parameters name: StringabsPath: StringReturns PakManager.MountStatus PakManager.MountStatus.Ok on success. On failure, returns one of the typed error codes — apps may want to prompt the user to update on PakManager.MountStatus.SchemaTooNew.
|
mount
(
name
, file
)
|
Convenience overload that takes a File.
Signature
fun mount(name: String, file: File): PakManager.MountStatus Parameters name: Stringfile: FileReturns PakManager.MountStatus |
openAssetFileDescriptor
(
uri
)
|
Returns an AssetFileDescriptor over the entry's bytes — a slice of the .pak file at the entry's offset+length. Returns null for DEFLATE entries (no contiguous file region exists), missing entries, or unknown mounts.
The textbook Android handoff for "play this slice of a packaged file": MediaPlayer, ExoPlayer.FileDataSource, SoundPool, and Bitmap decoders all accept AssetFileDescriptor directly and optimize for it (kernel-side seeks, no userspace copy of the payload).
The caller owns the returned descriptor and must close() it when done. Most media APIs dup the underlying FD so closing the AssetFileDescriptor is safe even while playback continues.
Signature
fun openAssetFileDescriptor(uri: String): AssetFileDescriptor? Parameters uri: StringReturns AssetFileDescriptor? |
openInputStream
(
context
, uri
)
|
Scheme-aware InputStream opener for use by Kotlin asset loaders. Replaces ad-hoc apk:// / file:// switches at call sites so they pick up pak:// for free.
Throws when the scheme is unsupported or the asset cannot be opened — the caller is expected to bracket with try/catch the same way it would for context.assets.open.
Signature
fun openInputStream(context: Context, uri: Uri): InputStream Parameters context: Contexturi: UriReturns InputStream |
qualify
(
pakName
, uri
)
|
Rewrites a pak://./<path> shorthand against the supplied PakManager.qualify, returning a fully-qualified pak://<pakName>/<path> URI. Inputs that do not start with pak://./ are returned unchanged. This is the explicit Kotlin counterpart to load-time rewrites the C++ runtime does inside the glXF parser.
Example:
audio.uri = PakManager.qualify("content", "pak://./audio/ping.wav")
// → "pak://content/audio/ping.wav"
Signature
fun qualify(pakName: String, uri: String): String Parameters pakName: Stringuri: StringReturns String |
readChunk
(
uri
, dst
, dstOffset
, srcOffset
, count
)
|
Streams up to PakManager.readChunk bytes from the entry at PakManager.readChunk, starting at byte offset PakManager.readChunk, into PakManager.readChunk[PakManager.readChunk..PakManager.readChunk+PakManager.readChunk). Returns the number of bytes actually copied (0 at EOF; fewer than PakManager.readChunk when PakManager.readChunk+PakManager.readChunk would overrun the entry), or -1 on error (unknown URI, missing entry, out-of-bounds destination range, CRC mismatch on the source, or DEFLATE entry — see below).
Currently STORE-mode entries only — chunked reads of compressed entries would need to carry inflate state across calls and aren't implemented yet. Callers that need to stream a DEFLATE entry should fall back to PakManager.readWhole. Most pak content (glTF/.glb, textures, audio, splat) is STORE for the mmap fast path, so this restriction rarely bites.
Use this for large assets (multi-MiB splat / audio) to avoid the readWhole double allocation (native vector + JVM ByteArray copy).
Signature
fun readChunk(uri: String, dst: ByteArray, dstOffset: Int, srcOffset: Long, count: Int): Int Parameters uri: Stringdst: ByteArraydstOffset: IntsrcOffset: Longcount: IntReturns Int |
readWhole
(
uri
)
|
Returns the raw bytes of the entry at PakManager.readWhole, or null if the URI is malformed, the mount is unknown, or the entry is missing.
Signature
fun readWhole(uri: String): ByteArray? Parameters uri: StringReturns ByteArray? |
setSchemaCeiling
(
ceiling
)
|
Sets the runtime BCV ceiling for schemaVersion. Paks whose manifest declares a higher version are rejected at PakManager.mount time. Default is Long.MAX_VALUE (accept everything).
Signature
fun setSchemaCeiling(ceiling: Long) Parameters ceiling: Long |
setSignatureKey
(
key
)
|
Registers an HMAC-SHA256 key for manifest signing. When set, every subsequent PakManager.mount call requires the pak to ship a pak.manifest.sig entry whose contents are the raw 32-byte HMAC over the pak.manifest.json bytes computed with this key. Mounts that fail verification surface as PakManager.MountStatus.SignatureInvalid / PakManager.MountStatus.SignatureMissing / PakManager.MountStatus.SignatureKeyMissing.
Pass an empty array to disable signing checks (the pre-D11 default).
Apps that distribute downloadable content packs typically call this once at startup, before any mount(), with a key derived from the app's signing certificate or a per-app secret stored in NDK strings / a server-side handshake.
Signature
fun setSignatureKey(key: ByteArray) Parameters key: ByteArray |
unmount
(
name
)
|
Unmounts PakManager.unmount. Returns false if no mount was registered under PakManager.unmount.
Signature
fun unmount(name: String): Boolean Parameters name: StringReturns Boolean |
FileChannel.map() the bytes themselves — used by PakManager.mapWhole for the splat / texture / audio zero-copy paths and by AssetFileDescriptor-based media playback.
(Ljava/lang/String;JJZ)V) — keep the field order and types in lock-step with JavaAPIs_Pak.cpp.
data class EntryRegion(val filePath: String, val offset: Long, val size: Long, val isStore: Boolean)
EntryRegion
(
filePath
, offset
, size
, isStore
)
|
Signature
constructor(filePath: String, offset: Long, size: Long, isStore: Boolean) Parameters offset: Long
Byte offset of the entry's data within filePath.
size: Long
Declared uncompressed size, in bytes.
isStore: Boolean
True for STORE-mode entries (safe to slice / mmap directly); false for DEFLATE entries (use PakManager.readWhole instead).
Returns PakManager.EntryRegion |
filePath
: String
[Get] |
Absolute path to the .pak file on disk (the same path passed to PakManager.mount).
Signature
val filePath: String |
isStore
: Boolean
[Get] |
True for STORE-mode entries (safe to slice / mmap directly); false for DEFLATE entries (use PakManager.readWhole instead).
Signature
val isStore: Boolean |
offset
: Long
[Get] |
Byte offset of the entry's data within filePath.
Signature
val offset: Long |
size
: Long
[Get] |
Declared uncompressed size, in bytes.
Signature
val size: Long |
MountStatus enum.
enum MountStatus : Enum<PakManager.MountStatus>
| Member | Description |
|---|---|
Ok | |
AlreadyMounted | |
OpenFailed | |
ManifestMissing | |
ManifestInvalid | |
SchemaTooNew | |
SchemaTooOld | |
EntryTooLarge |
A single entry's declared uncompressed size exceeds the per-entry cap.
|
AggregateTooLarge |
Sum of declared uncompressed sizes exceeds the per-mount cap.
|
UnsafeEntryPath |
An entry's name fails path-safety (e.g. contains .. or is absolute).
|
EntryNameTooLong |
An entry's name exceeds the registry's filename length budget.
|
SignatureKeyMissing |
The pak ships a pak.manifest.sig but no signature key is registered. Either the pak was built signed for a key the embedder hasn't loaded, or PakManager.setSignatureKey needs to be called before PakManager.mount.
|
SignatureMissing |
A signature key is registered but the pak doesn't ship a pak.manifest.sig. Apps that opt into signing don't accept unsigned content.
|
SignatureInvalid |
HMAC-SHA256 over pak.manifest.json doesn't match the signature shipped in pak.manifest.sig. The pak is corrupt or tampered.
|
EntryCrcMismatch |
A STORE entry's bytes failed CRC-32 verification at mount time (Diff 18). The pak file is corrupt or tampered. Apps may want to delete the file and re-download.
|
Unknown |
code
: Int
[Get] |
Signature
val code: Int |
isOk
: Boolean
[Get] |
Signature
val isOk: Boolean |