mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Epic: the engine takes the chunk cache path, and assembles the files itself
EpicNative.run passes chunkCacheDir after installDir, as the contract now has it: "" keeps the cache beside the game, a path is the scratch cache in the app's own storage that an install to a card uses. EpicNative.assemble wraps the new nativeAssemble the same blocking way: the pending files are written from the cache and each chunk goes right after its last use, then the folder. The manager hands both runs the same path, uses the assembler when it starts and keeps its own write loop as the fallback; the engine's cache no longer has to sit beside a game on a card.
This commit is contained in:
1 parent
35dfd99013
commit
fbd068c6b8
4 files changed
+109
-53
No files matched your search
@@ -138,12 +138,10 @@ public final class EpicDownloadManager {
|
||||
|
||||
File installDir = new File(installDirPath);
|
||||
installDir.mkdirs();
|
||||
// The native engine keeps its cache beside the game (its contract names <installDir>/.chunks);
|
||||
// the scratch path is honoured by the Java pool and the assembly. A path on a card and a
|
||||
// native run therefore still cache on the card until the engine takes the parameter.
|
||||
boolean scratch = chunkCacheDirPath != null && !chunkCacheDirPath.isEmpty();
|
||||
File engineCacheDir = new File(installDir, ".chunks");
|
||||
File chunkCacheDir = scratch ? new File(chunkCacheDirPath) : engineCacheDir;
|
||||
// "" = the cache beside the game; a path = the scratch cache (the app's own storage when
|
||||
// the game goes to a card). The engine and the Java loops read the same value.
|
||||
final String cachePath = chunkCacheDirPath == null ? "" : chunkCacheDirPath;
|
||||
final File chunkCacheDir = cachePath.isEmpty() ? new File(installDir, ".chunks") : new File(cachePath);
|
||||
chunkCacheDir.mkdirs();
|
||||
|
||||
List<FileInfo> selected = resolveInstallFiles(manifest, installTags);
|
||||
@@ -185,12 +183,13 @@ public final class EpicDownloadManager {
|
||||
cb.onLog("epic: " + fmt(fTotalBytes) + " in " + totalChunks + " chunks");
|
||||
|
||||
boolean javaPool = true;
|
||||
if (com.droiddeck.launcher.stores.StoresNative.INSTANCE.getAvailable()) {
|
||||
NativeOutcome r = runNativePool(ctx, manifestBytes, manifest, pending, needed, fTotalBytes, installDirPath, cdnUrls, cancel, cb,
|
||||
final int[] pendingIdx = pendingIndices(manifest, pending);
|
||||
boolean engine = com.droiddeck.launcher.stores.StoresNative.INSTANCE.getAvailable() && pendingIdx != null;
|
||||
if (engine) {
|
||||
NativeOutcome r = runNativePool(ctx, manifestBytes, pendingIdx, needed, fTotalBytes, installDirPath, cachePath, cdnUrls, cancel, cb,
|
||||
completedBytes, completedCount, lastSpeedMs, lastSpeedBytes, speedBps);
|
||||
if (r.started) {
|
||||
javaPool = false;
|
||||
chunkCacheDir = engineCacheDir;
|
||||
if (r.cancelled) return null;
|
||||
if (!r.success) { cb.onLog("epic: engine failed: " + r.error); failCount.incrementAndGet(); }
|
||||
} else cb.onLog("epic: engine not started (" + r.error + "); using the built-in pool");
|
||||
@@ -198,12 +197,11 @@ public final class EpicDownloadManager {
|
||||
if (javaPool) {
|
||||
cb.onLog("epic: engine=built-in (8 threads)");
|
||||
ExecutorService pool = Executors.newFixedThreadPool(8);
|
||||
final File poolCacheDir = chunkCacheDir;
|
||||
for (ChunkInfo chunk : needed) {
|
||||
final ChunkInfo fc = chunk;
|
||||
pool.submit(() -> {
|
||||
if (cancel.get()) return;
|
||||
File cached = new File(poolCacheDir, fc.guidStr());
|
||||
File cached = new File(chunkCacheDir, fc.guidStr());
|
||||
if (!cached.exists() && !downloadChunkStreaming(fc, manifest.chunkDir, cdnUrls, cached)) {
|
||||
cb.onLog("FAIL chunk=" + fc.guidStr());
|
||||
failCount.incrementAndGet();
|
||||
@@ -230,6 +228,18 @@ public final class EpicDownloadManager {
|
||||
if (cancel.get()) return null;
|
||||
if (failCount.get() > 0) throw new InstallException(failCount.get() + " chunk(s) failed to download");
|
||||
|
||||
// The engine's assembler when it starts (it drops each chunk after its last use and the
|
||||
// cache at the end); the loop below when it does not.
|
||||
if (engine) {
|
||||
NativeOutcome r = runNativeAssembly(manifestBytes, pendingIdx, pending.size(), installDirPath, cachePath, cancel, cb);
|
||||
if (r.started) {
|
||||
if (r.cancelled) return null;
|
||||
if (!r.success) throw new InstallException(r.error.isEmpty() ? "assembly failed" : r.error);
|
||||
cb.onProgress("Complete", 100);
|
||||
return new Result(manifest.launchExe, manifest.buildVersion, planned);
|
||||
}
|
||||
cb.onLog("epic: assembler not started (" + r.error + "); writing the files here");
|
||||
}
|
||||
int totalFiles = pending.size(), doneFiles = 0;
|
||||
for (FileInfo file : pending) {
|
||||
if (cancel.get()) return null;
|
||||
@@ -248,8 +258,7 @@ public final class EpicDownloadManager {
|
||||
}
|
||||
doneFiles++;
|
||||
}
|
||||
deleteDir(engineCacheDir);
|
||||
if (scratch) deleteDir(new File(chunkCacheDirPath));
|
||||
deleteDir(chunkCacheDir);
|
||||
cb.onProgress("Complete", 100);
|
||||
return new Result(manifest.launchExe, manifest.buildVersion, planned);
|
||||
} catch (InstallException e) {
|
||||
@@ -262,28 +271,33 @@ public final class EpicDownloadManager {
|
||||
|
||||
private static final class NativeOutcome { boolean started, success, cancelled; String error = ""; }
|
||||
|
||||
/** The pending files as indices into the manifest's file list, what both native runs take; null when one is not in it. */
|
||||
private static int[] pendingIndices(Manifest manifest, List<FileInfo> pending) {
|
||||
java.util.IdentityHashMap<FileInfo, Integer> index = new java.util.IdentityHashMap<>();
|
||||
for (int i = 0; i < manifest.files.size(); i++) index.put(manifest.files.get(i), i);
|
||||
int[] pendingIdx = new int[pending.size()];
|
||||
for (int i = 0; i < pending.size(); i++) {
|
||||
Integer k = index.get(pending.get(i));
|
||||
if (k == null) return null;
|
||||
pendingIdx[i] = k;
|
||||
}
|
||||
return pendingIdx;
|
||||
}
|
||||
|
||||
/** The native chunk pool on the same counters; never throws. */
|
||||
private static NativeOutcome runNativePool(Context ctx, byte[] manifestBytes, Manifest manifest, List<FileInfo> pending, List<ChunkInfo> needed, long totalBytes,
|
||||
String installDirPath, List<CdnUrl> cdnUrls, AtomicBoolean cancel, Callback cb,
|
||||
private static NativeOutcome runNativePool(Context ctx, byte[] manifestBytes, int[] pendingIdx, List<ChunkInfo> needed, long totalBytes,
|
||||
String installDirPath, String cachePath, List<CdnUrl> cdnUrls, AtomicBoolean cancel, Callback cb,
|
||||
AtomicLong completedBytes, AtomicInteger completedCount, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) {
|
||||
NativeOutcome r = new NativeOutcome();
|
||||
try {
|
||||
java.util.IdentityHashMap<FileInfo, Integer> index = new java.util.IdentityHashMap<>();
|
||||
for (int i = 0; i < manifest.files.size(); i++) index.put(manifest.files.get(i), i);
|
||||
int[] pendingIdx = new int[pending.size()];
|
||||
for (int i = 0; i < pending.size(); i++) {
|
||||
Integer k = index.get(pending.get(i));
|
||||
if (k == null) { r.error = "pending file not in manifest"; return r; }
|
||||
pendingIdx[i] = k;
|
||||
}
|
||||
String[] prefixes = new String[cdnUrls.size()];
|
||||
for (int i = 0; i < cdnUrls.size(); i++) prefixes[i] = cdnUrls.get(i).baseUrl + cdnUrls.get(i).cloudDir;
|
||||
final int totalChunks = needed.size();
|
||||
StoreDownloadTier tier = StoreDownloadTier.Companion.current(ctx);
|
||||
int workers = Math.max(1, Math.min(128, tier.getNetworkWindow()));
|
||||
int process = Math.max(2, tier.getProcessWorkers());
|
||||
cb.onLog("epic: engine=native workers=" + workers + " process_workers=" + process + " tier=" + tier.getId());
|
||||
EpicNative.Result res = EpicNative.run(manifestBytes, installDirPath, prefixes, pendingIdx, totalChunks, totalBytes, "", workers, process, cancel,
|
||||
cb.onLog("epic: engine=native workers=" + workers + " process_workers=" + process + " tier=" + tier.getId() + (cachePath.isEmpty() ? "" : " cache=scratch"));
|
||||
EpicNative.Result res = EpicNative.run(manifestBytes, installDirPath, cachePath, prefixes, pendingIdx, totalChunks, totalBytes, "", workers, process, cancel,
|
||||
new EpicNative.Listener() {
|
||||
@Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) { cb.onLog("epic: plan chunks=" + chunksTotal + " bytes=" + bytesTotal + " dir=" + chunkDir); }
|
||||
@Override public void onProgress(long bytesDone, long bytesTotal, int chunksDone, int chunksTotal) {
|
||||
@@ -304,6 +318,27 @@ public final class EpicDownloadManager {
|
||||
return r;
|
||||
}
|
||||
|
||||
/** The native assembler: the pending files written from the cache, the cache removed on success; never throws. */
|
||||
private static NativeOutcome runNativeAssembly(byte[] manifestBytes, int[] pendingIdx, int totalFiles, String installDirPath, String cachePath, AtomicBoolean cancel, Callback cb) {
|
||||
NativeOutcome r = new NativeOutcome();
|
||||
try {
|
||||
cb.onLog("epic: assembler=native files=" + totalFiles + (cachePath.isEmpty() ? "" : " cache=scratch"));
|
||||
EpicNative.Result res = EpicNative.assemble(manifestBytes, installDirPath, cachePath, pendingIdx, cancel, new EpicNative.Listener() {
|
||||
@Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) {}
|
||||
@Override public void onProgress(long bytesDone, long bytesTotal, int done, int total) {
|
||||
cb.onProgress("Writing files (" + done + "/" + Math.max(total, totalFiles) + ")", 80 + (int) (done * 20L / Math.max(1, Math.max(total, totalFiles))));
|
||||
}
|
||||
@Override public void onLog(String line) { cb.onLog(line); }
|
||||
@Override public void onComplete(boolean success, String error, long bytes) {}
|
||||
});
|
||||
r.started = res.started; r.success = res.success; r.cancelled = res.cancelled; r.error = res.error == null ? "" : res.error;
|
||||
} catch (Throwable t) {
|
||||
r.started = false;
|
||||
r.error = t.getClass().getSimpleName() + ": " + t.getMessage();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- CDN list and manifest download ---------------------------------------------------------
|
||||
|
||||
public static List<CdnUrl> parseCdnUrls(String json) {
|
||||
|
||||
@@ -9,41 +9,56 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* The native Epic chunk fetcher in `libdroiddeckstores.so`: it fills `<installDir>/.chunks/<GUID>`
|
||||
* with verified, decompressed chunks for the files the manager says are pending. Everything
|
||||
* around it (manifest, install tags, delta pass, assembly, post-install) stays in the manager.
|
||||
* The native half of an Epic install in `libdroiddeckstores.so`: [run] fills the chunk cache with
|
||||
* verified, decompressed chunks for the files the manager says are pending, [assemble] writes those
|
||||
* files from the cache. Everything around them (manifest, install tags, delta pass, post-install)
|
||||
* stays in the manager.
|
||||
*
|
||||
* [run] blocks like the Java pool it replaces and polls [AtomicBoolean] `cancel` every 250 ms: on
|
||||
* cancel it flips the native flag, waits up to 5 s for the run to wind down, then returns.
|
||||
* The cache is `<installDir>/.chunks` when `chunkCacheDir` is `""`, else the scratch folder given -
|
||||
* the app's own cache when the game installs to a card, where the assembler drops each chunk right
|
||||
* after its last use and the folder at the end. Both calls take the same value for one install.
|
||||
*
|
||||
* Both block like the Java loops they replace and poll [AtomicBoolean] `cancel` every 250 ms: on
|
||||
* cancel they flip the native flag, wait up to 5 s for the run to wind down, then return.
|
||||
*/
|
||||
object EpicNative {
|
||||
private const val TAG = "EpicNative"
|
||||
|
||||
/** Callbacks from the native run; every method is called on a native thread. */
|
||||
/** Callbacks from a native run; every method is called on a native thread. One interface serves both runs. */
|
||||
interface Listener {
|
||||
/** Once, before any fetch: the plan the engine derived (the manager cross-checks it). */
|
||||
/** Fetch only, once before any fetch: the plan the engine derived (the manager cross-checks it). */
|
||||
fun onPlan(chunksTotal: Int, bytesTotal: Long, chunkDir: String)
|
||||
/** Per accounted chunk (cached-skip or fetched). */
|
||||
fun onProgress(bytesDone: Long, bytesTotal: Long, chunksDone: Int, chunksTotal: Int)
|
||||
/** Fetch: per accounted chunk (cached-skip or fetched), done/total = chunks. Assembly: per file written, done/total = files. */
|
||||
fun onProgress(bytesDone: Long, bytesTotal: Long, done: Int, total: Int)
|
||||
/** Engine log line (already in logcat under `EpicNative`). */
|
||||
fun onLog(line: String)
|
||||
/** Terminal. */
|
||||
fun onComplete(success: Boolean, error: String, bytesCredited: Long)
|
||||
/** Terminal. Fetch: bytes credited; assembly: bytes written. */
|
||||
fun onComplete(success: Boolean, error: String, bytes: Long)
|
||||
}
|
||||
|
||||
/** Outcome of [run]. `started == false`: nothing was fetched, the caller runs its own pool. */
|
||||
/** Outcome of a run. `started == false`: nothing happened on disk, the caller runs its own loop. */
|
||||
class Result(
|
||||
@JvmField val started: Boolean, @JvmField val success: Boolean, @JvmField val cancelled: Boolean,
|
||||
@JvmField val error: String, @JvmField val bytesCredited: Long, @JvmField val chunksDone: Int, @JvmField val chunksTotal: Int,
|
||||
@JvmField val error: String, @JvmField val bytes: Long, @JvmField val done: Int, @JvmField val total: Int,
|
||||
)
|
||||
|
||||
private class Completion(val success: Boolean, val error: String, val bytes: Long)
|
||||
|
||||
/** The fetch: chunks for [pendingFileIdx] into the cache. */
|
||||
@JvmStatic
|
||||
fun run(
|
||||
manifest: ByteArray, installDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long,
|
||||
manifest: ByteArray, installDir: String, chunkCacheDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long,
|
||||
caBundlePath: String, maxWorkers: Int, processWorkers: Int, cancel: AtomicBoolean?, listener: Listener,
|
||||
): Result {
|
||||
): Result = drive("nativeStart", cancel, listener) { inner ->
|
||||
nativeStart(manifest, installDir, chunkCacheDir, cdnPrefixes, pendingFileIdx, expectedChunks, expectedBytes, caBundlePath, maxWorkers, processWorkers, inner)
|
||||
}
|
||||
|
||||
/** The assembly: the files [fileIdx] written from the cache, the cache removed on success. */
|
||||
@JvmStatic
|
||||
fun assemble(manifest: ByteArray, installDir: String, chunkCacheDir: String, fileIdx: IntArray, cancel: AtomicBoolean?, listener: Listener): Result =
|
||||
drive("nativeAssemble", cancel, listener) { inner -> nativeAssemble(manifest, installDir, chunkCacheDir, fileIdx, inner) }
|
||||
|
||||
private fun drive(what: String, cancel: AtomicBoolean?, listener: Listener, start: (Listener) -> Long): Result {
|
||||
if (!StoresNative.ensureLoaded()) return Result(false, false, false, "engine not built", 0L, 0, 0)
|
||||
val latch = CountDownLatch(1)
|
||||
val completion = AtomicReference<Completion?>(null)
|
||||
@@ -51,19 +66,19 @@ object EpicNative {
|
||||
val totalRef = AtomicReference(0)
|
||||
val inner = object : Listener {
|
||||
override fun onPlan(chunksTotal: Int, bytesTotal: Long, chunkDir: String) { totalRef.set(chunksTotal); listener.onPlan(chunksTotal, bytesTotal, chunkDir) }
|
||||
override fun onProgress(bytesDone: Long, bytesTotal: Long, chunksDone: Int, chunksTotal: Int) { doneRef.set(chunksDone); listener.onProgress(bytesDone, bytesTotal, chunksDone, chunksTotal) }
|
||||
override fun onProgress(bytesDone: Long, bytesTotal: Long, done: Int, total: Int) { doneRef.set(done); totalRef.set(total); listener.onProgress(bytesDone, bytesTotal, done, total) }
|
||||
override fun onLog(line: String) = listener.onLog(line)
|
||||
override fun onComplete(success: Boolean, error: String, bytesCredited: Long) {
|
||||
completion.set(Completion(success, error, bytesCredited))
|
||||
override fun onComplete(success: Boolean, error: String, bytes: Long) {
|
||||
completion.set(Completion(success, error, bytes))
|
||||
latch.countDown()
|
||||
runCatching { listener.onComplete(success, error, bytesCredited) }
|
||||
runCatching { listener.onComplete(success, error, bytes) }
|
||||
}
|
||||
}
|
||||
val handle: Long = try {
|
||||
nativeStart(manifest, installDir, cdnPrefixes, pendingFileIdx, expectedChunks, expectedBytes, caBundlePath, maxWorkers, processWorkers, inner)
|
||||
start(inner)
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "engine unavailable: ${t.javaClass.simpleName}: ${t.message}")
|
||||
return Result(false, false, false, "nativeStart: ${t.javaClass.simpleName}", 0L, 0, 0)
|
||||
return Result(false, false, false, "$what: ${t.javaClass.simpleName}", 0L, 0, 0)
|
||||
}
|
||||
if (handle == 0L) return Result(false, false, false, completion.get()?.error ?: "not started", 0L, 0, totalRef.get())
|
||||
var cancelSent = false
|
||||
@@ -87,10 +102,13 @@ object EpicNative {
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeStart(
|
||||
manifest: ByteArray, installDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long,
|
||||
manifest: ByteArray, installDir: String, chunkCacheDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long,
|
||||
caBundlePath: String, maxWorkers: Int, processWorkers: Int, listener: Listener,
|
||||
): Long
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeAssemble(manifest: ByteArray, installDir: String, chunkCacheDir: String, fileIdx: IntArray, listener: Listener): Long
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeCancel(handle: Long)
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ scanned (`StoreInstallRoot.roots`): internal, every card, and the Steam library'
|
||||
installs an earlier build put there. `<Store>` is `GOG`, `Epic` or `Amazon`; `<title>` is the title
|
||||
with unsafe characters dropped, at most 60 characters (`StoreInstallRoot.folderName`), chosen once;
|
||||
a rerun (repair, update) lands on the folder whose sidecar carries the game's id, wherever it is.
|
||||
An Epic install on a card keeps its in-flight chunks in `cacheDir/stores/epic/<id>/` (the Java pool
|
||||
and the assembly honour it; the native engine's own cache stays `<installDir>/.chunks` until its
|
||||
contract takes a cache path); the scratch folder is removed on cancel and uninstall.
|
||||
An Epic install on a card keeps its in-flight chunks in `cacheDir/stores/epic/<id>/`: the
|
||||
`chunkCacheDir` of both native calls (`EpicNative.run` fetches into it, `EpicNative.assemble`
|
||||
writes the files from it and drops each chunk after its last use, then the folder), and of the
|
||||
manager's own loops when the engine is not there; `""` keeps the cache beside the game, as an
|
||||
internal install has it. The scratch folder is removed on cancel-with-delete and on uninstall.
|
||||
|
||||
## The sidecar
|
||||
|
||||
|
||||
@@ -111,9 +111,10 @@ without it and Setup says "Stores engine not built").
|
||||
already registered there keep their appid); every root is scanned and uninstall works for
|
||||
either. The Downloads
|
||||
row shows where a game is going, with its art as a small 16:9 thumbnail; the game page names the
|
||||
place. Epic installs on a card keep their in-flight chunks in the app's cache (Java pool and
|
||||
assembly; the native engine's `chunkCacheDir` parameter is not in JNI.md yet and is wired the
|
||||
moment it lands), cleaned on cancel and uninstall.
|
||||
place. Epic installs on a card keep their in-flight chunks in the app's cache: the engine's
|
||||
`chunkCacheDir` (fetch and the new `nativeAssemble`, which drops each chunk after its last use)
|
||||
and the manager's own loops read the same path; internal installs pass `""` as before. Cleaned on
|
||||
cancel-with-delete and uninstall.
|
||||
- **Device-proven on the AYANEO Pocket FIT, 2026-10-08 (GOG):** sign-in, the library (33 owned),
|
||||
DOOM I Enhanced installed through the GOG engine to the SD Games root, shown in Games with the
|
||||
GOG chip and real art, registered as a non-Steam game in the Steam client (grid art present), and
|
||||
|
||||
Reference in new issue
Block a user