@@ -236,7 +236,10 @@ void ImageEngine::DispatchImageLoad(const std::wstring& path, ImageID imageId, u
236236 // Scene A: Small JXL (< 1MB AND < 2MP) -> FastLane Direct Full Decode
237237 // 1MB = 1048576 bytes
238238 // 2MP = 2000000 pixels
239- bool isSmall = (info.fileSize < 1048576 ) && ((uint64_t )info.width * info.height < 2000000 );
239+ // [v9.2] Fix: Check for valid dimensions! (PeekHeader fail = 0x0)
240+ bool isSmall = (info.fileSize < 1048576 ) &&
241+ (info.width > 0 && info.height > 0 ) &&
242+ ((uint64_t )info.width * info.height < 2000000 );
240243
241244 if (isSmall) {
242245 OutputDebugStringW (L" [Dispatch] -> JXL Small: FastLane Direct Full\n " );
@@ -408,6 +411,9 @@ std::vector<EngineEvent> ImageEngine::PollState() {
408411 } else if (e.type == EventType::FullReady && !e.isScaled ) {
409412 m_isViewingScaledImage = false ; // Final reached
410413 m_stage2Requested = false ; // Reset request flag (job done)
414+
415+ // [v9.0] Startup Delay Check
416+ CheckStartupDelay ();
411417 } else if (e.type == EventType::LoadError) {
412418 // [JXL Scene C] FastLane Aborted (Modular?) -> Trigger Heavy Immediately
413419 if (m_pendingJxlHeavyId == e.imageId && m_pendingJxlHeavyId != 0 ) {
@@ -419,6 +425,14 @@ std::vector<EngineEvent> ImageEngine::PollState() {
419425 }
420426 }
421427
428+
429+
430+ // [v9.2] Fix: Clean up pending paths on Error too (Fixes Blue Light Forever)
431+ if (e.type == EventType::LoadError) {
432+ std::lock_guard lock (m_pendingMutex);
433+ m_pendingPaths.erase (e.filePath );
434+ }
435+
422436 // [v8.11 Fix] Cache ALL FullReady events (both current and prefetch)
423437 // Previously, only non-current (prefetch) images were cached.
424438 // This caused the bug where viewed images weren't cached for return navigation.
@@ -450,6 +464,9 @@ std::vector<EngineEvent> ImageEngine::PollState() {
450464 }
451465 }
452466
467+ // [v9.1] Pump Serial Prefetch Queue
468+ PumpPrefetch ();
469+
453470 return batch;
454471}
455472
@@ -649,13 +666,8 @@ ImageEngine::TelemetrySnapshot ImageEngine::GetTelemetry() const {
649666
650667void ImageEngine::ResetDebugCounters () {
651668 m_fastLane.ResetSkipCount ();
652- m_fastLane.ResetSkipCount ();
653- }
654-
655- // Note: IsFastPassCandidate removed in v3.1 - replaced by PeekHeader() classification
656-
657-
658669
670+ }
659671
660672// ============================================================================
661673// Fast Lane (The Recon)
@@ -995,47 +1007,41 @@ void ImageEngine::UpdateView(int currentIndex, BrowseDirection dir) {
9951007 // [v3.1] Cancellation Strategy: Ruthless Purge -> Reschedule
9961008 // ------------------------------------------------------------------------
9971009
998- // 1. [v8.10 Fix] REMOVED m_fastLane.Clear()!
999- // Clear() was too aggressive - it cleared the current image before the worker could process it.
1000- // FastLane is LIFO, so new items are prioritized anyway.
1001- // Stale prefetch items will be naturally evicted by cache memory limits.
1002- // m_fastLane.Clear(); // REMOVED - causes blank screen on FastLane items
1003-
1004- // 2. [v8.9 Fix] Current index is ALREADY dispatched by NavigateTo->DispatchImageLoad.
1005- // DO NOT reschedule it here! Doing so causes duplicate HeavyPool submissions,
1006- // which leads to two workers decoding the same image simultaneously (wasteful + race conditions).
1007- // ScheduleJob(currentIndex, Priority::Critical); // REMOVED - causes duplicate decode
1008-
10091010 // 4. If prefetch disabled, stop here
10101011 if (!m_prefetchPolicy.enablePrefetch ) return ;
10111012
1013+ // [v9.1] Serial Queue Population
1014+ m_prefetchQueue.clear ();
1015+
10121016 if (dir == BrowseDirection::IDLE ) {
10131017 // [Startup/Reset] Conservative Bidirectional Prefetch
1014- // [v8.8] Fix: Handle wrapping (Previous of 0 -> count-1) using modulo logic
10151018 int count = (int )m_navigator->Count ();
10161019 if (count > 0 ) {
10171020 int nextIdx = (currentIndex + 1 ) % count;
10181021 int prevIdx = (currentIndex - 1 + count) % count;
10191022
1020- // Only fetch immediate neighbors (+/- 1) to save resources.
1021- ScheduleJob ( nextIdx, Priority::High);
1022- ScheduleJob ( prevIdx, Priority::High);
1023+ // Queue neighbors
1024+ m_prefetchQueue. push_back ({ nextIdx, Priority::High} );
1025+ m_prefetchQueue. push_back ({ prevIdx, Priority::High} );
10231026 }
10241027 } else {
10251028 // Directional Prefetch
10261029 int step = (dir == BrowseDirection::BACKWARD ) ? -1 : 1 ;
10271030
10281031 // 3. Adjacent: High Priority
1029- ScheduleJob ( currentIndex + step, Priority::High);
1032+ m_prefetchQueue. push_back ({ currentIndex + step, Priority::High} );
10301033
10311034 // 5. Anti-regret: One in opposite direction
1032- ScheduleJob ( currentIndex - step, Priority::Low);
1035+ m_prefetchQueue. push_back ({ currentIndex - step, Priority::Low} );
10331036
10341037 // 6. Look-ahead
10351038 for (int i = 2 ; i <= m_prefetchPolicy.lookAheadCount ; ++i) {
1036- ScheduleJob ( currentIndex + step * i, Priority::Idle);
1039+ m_prefetchQueue. push_back ({ currentIndex + step * i, Priority::Idle} );
10371040 }
10381041 }
1042+
1043+ // Pump queue immediately
1044+ PumpPrefetch ();
10391045}
10401046
10411047void ImageEngine::ScheduleJob (int index, Priority pri) {
@@ -1053,11 +1059,12 @@ void ImageEngine::ScheduleJob(int index, Priority pri) {
10531059 if (m_cache.count (path)) return ; // Already cached
10541060 }
10551061
1056- // 4. Critical priority: Allow re-queueing (fixes UpdateView clearing queue)
1057- // if (pri == Priority::Critical) return;
1058-
1059- // 5. Allow all priorities (High/Low/Idle) based on UpdateView loop
1060- // if (pri != Priority::High) return;
1062+ // [v9.0] Strict Startup Delay
1063+ // If startup prefetch is not allowed yet, BLOCK all non-Critical jobs.
1064+ // Critical job (Current Image) is allowed always.
1065+ if (!m_startupPrefetchAllowed && pri != Priority::Critical) {
1066+ return ;
1067+ }
10611068
10621069 // [v4.1] Smart Prefetch logic re-enabled (Unified Dispatch Integration)
10631070
@@ -1066,6 +1073,25 @@ void ImageEngine::ScheduleJob(int index, Priority pri) {
10661073
10671074 // 7. Dispatch based on classification
10681075 uintmax_t fileSize = m_navigator->GetFileSize (index);
1076+
1077+ // [v9.4] Smart Skip: If single image > Cache Cap, skip prefetch
1078+ // This prevents "Eco Mode OOM" where a single 90MP image (350MB)
1079+ // forces overflow despite the 128MB limit.
1080+ if (pri != Priority::Critical && m_prefetchPolicy.maxCacheMemory > 0 ) {
1081+ uint64_t predictedSize = (uint64_t )info.width * info.height * 4 ;
1082+ // Allow a 10% margin just in case, but strictly reject if it consumes > 90% of ENTIRE cache
1083+ // Actually, user agreed to > 80% rule or Strict Cap.
1084+ // Let's use Strict Cap to be safe for Eco Mode.
1085+ if (predictedSize > m_prefetchPolicy.maxCacheMemory ) {
1086+ wchar_t skipBuf[256 ];
1087+ swprintf_s (skipBuf, L" [ImageEngine] Smart Skip: %s (%.1f MB) > Cache Cap (%.1f MB) -> Skipped\n " ,
1088+ path.substr (path.find_last_of (L" \\ /" ) + 1 ).c_str (),
1089+ predictedSize / 1048576.0 ,
1090+ m_prefetchPolicy.maxCacheMemory / 1048576.0 );
1091+ OutputDebugStringW (skipBuf);
1092+ return ;
1093+ }
1094+ }
10691095
10701096 if (info.type == CImageLoader::ImageType::TypeA_Sprint) {
10711097 // Small image: push to FastLane
@@ -1078,12 +1104,20 @@ void ImageEngine::ScheduleJob(int index, Priority pri) {
10781104 // Large image:
10791105 // Critical: Always submit
10801106 // Prefetch: Only if Heavy Lane is idle to avoid blocking Critical
1107+ // Prefetch: Only if Heavy Lane is idle to avoid blocking Critical
10811108 if (pri == Priority::Critical || m_heavyPool->IsIdle ()) {
10821109 {
10831110 std::lock_guard lock (m_pendingMutex);
10841111 m_pendingPaths.insert (path);
10851112 }
1086- m_heavyPool->Submit (path, ComputePathHash (path)); // [ImageID]
1113+
1114+ // [v9.3] Alignment: JXL uses Direct Full Decode (Two-Stage Cancelled).
1115+ // Prefetch must also be Full to prevent stuck Scaled/Blurry image.
1116+ if (info.format == L" JXL" ) {
1117+ m_heavyPool->SubmitFullDecode (path, ComputePathHash (path));
1118+ } else {
1119+ m_heavyPool->Submit (path, ComputePathHash (path)); // [ImageID]
1120+ }
10871121 }
10881122 // If Heavy is busy and not critical, skip prefetch
10891123 }
@@ -1291,3 +1325,78 @@ void ImageEngine::RequestFullMetadata() {
12911325
12921326 }).detach ();
12931327}
1328+
1329+ // [v9.0] Strict Startup Delay
1330+ void ImageEngine::CheckStartupDelay () {
1331+ // Only run if not yet allowed
1332+ if (m_startupPrefetchAllowed) return ;
1333+
1334+ // Launch detached thread to wait 500ms
1335+ std::thread ([this ]() {
1336+ std::this_thread::sleep_for (std::chrono::milliseconds (500 ));
1337+ m_startupPrefetchAllowed = true ;
1338+
1339+ // Trigger prefetch pump
1340+ // Note: UpdateView requires direction, but we can just poke the engine to retry
1341+ // Since UpdateView stores state, we might need a way to re-trigger.
1342+ // Actually, ScheduleJob called by UpdateView failed due to flag.
1343+ // We need to re-invoke UpdateView with current state.
1344+
1345+ // However, safely calling UpdateView from this thread requires ensuring it doesn't race.
1346+ // UpdateView is generally safe.
1347+ // Better: Queue a dummy event to wake up Main Loop which calls UpdateView?
1348+ // Or just let the next interaction handle it?
1349+ // User requirement: "Startup -> 500ms -> Start Prefetch". Automatic.
1350+
1351+ // Let's rely on the main thread to notice? No, main thread is idle.
1352+ // We must push an event or callback.
1353+ // Simple hack: Re-call UpdateView with current state.
1354+ int idx = m_currentViewIndex.load ();
1355+ int dirInt = m_lastDirectionInt.load ();
1356+ BrowseDirection dir = (dirInt == 1 ) ? BrowseDirection::FORWARD :
1357+ (dirInt == -1 ) ? BrowseDirection::BACKWARD : BrowseDirection::IDLE ;
1358+
1359+ if (idx >= 0 ) {
1360+ UpdateView (idx, dir);
1361+ }
1362+ }).detach ();
1363+ }
1364+
1365+ // [v9.1] Serial Prefetch Pump
1366+ void ImageEngine::PumpPrefetch () {
1367+ if (m_prefetchQueue.empty ()) return ;
1368+ if (!m_startupPrefetchAllowed) return ; // Blocked by startup delay
1369+
1370+ // Process queue until we find work or run out
1371+ while (!m_prefetchQueue.empty ()) {
1372+ // Strict Serial Check: Is ANY engine working?
1373+ // Check HeavyPool
1374+ if (!m_heavyPool->IsIdle ()) return ;
1375+
1376+ // Check FastLane (accessing internal state via friend/member)
1377+ if (m_fastLane.GetQueueSize () > 0 || m_fastLane.m_isWorking .load ()) return ;
1378+
1379+ auto task = m_prefetchQueue.front ();
1380+ m_prefetchQueue.pop_front ();
1381+
1382+ // Check bounds
1383+ if (!m_navigator || task.index < 0 || task.index >= (int )m_navigator->Count ()) continue ;
1384+
1385+ // Check cache before scheduling to avoid "scheduling nothing" and stopping
1386+ std::wstring path = m_navigator->GetFile (task.index );
1387+ {
1388+ std::lock_guard lock (m_cacheMutex);
1389+ if (m_cache.count (path)) continue ; // Already cached, try next
1390+ }
1391+
1392+ // Schedule it
1393+ ScheduleJob (task.index , task.priority );
1394+
1395+ // We assume work started (or was queued).
1396+ // Since we checked IsIdle above, and ScheduleJob submits,
1397+ // the engine should now be BUSY (or queued).
1398+ // So we return to wait for it to finish.
1399+ return ;
1400+ }
1401+ }
1402+
0 commit comments