Skip to content

Commit 26dc089

Browse files
committed
Fix: JXL Full Decode logic, Smart Skip Prefetch strategy, Exif Auto-Rotation, and Code Cleanup
1 parent 48d384f commit 26dc089

7 files changed

Lines changed: 222 additions & 65 deletions

File tree

QuickView/HeavyLanePool.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,3 +645,8 @@ void HeavyLanePool::GetWorkerSnapshots(WorkerSnapshot* outBuffer, int capacity,
645645
}
646646
*outCount = count;
647647
}
648+
649+
bool HeavyLanePool::IsIdle() const {
650+
std::lock_guard lock(m_poolMutex);
651+
return m_busyCount.load() == 0 && m_pendingJobs.empty();
652+
}

QuickView/HeavyLanePool.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class HeavyLanePool {
8181

8282
// Check if any worker is busy (for wait cursor logic)
8383
bool IsBusy() const { return m_busyCount.load() > 0; }
84-
bool IsIdle() const { return m_busyCount.load() == 0 && m_pendingJobs.empty(); }
84+
bool IsIdle() const;
8585

8686
private:
8787
// === Worker Structure ===

QuickView/ImageEngine.cpp

Lines changed: 140 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

650667
void 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

10411047
void 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+

QuickView/ImageEngine.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,20 @@ class ImageEngine {
353353
void ScheduleJob(int index, Priority pri);
354354
void PruneQueue(int currentIndex, BrowseDirection dir);
355355

356+
void CheckStartupDelay(); // [v9.0] Enable prefetch after 500ms
357+
std::atomic<bool> m_startupPrefetchAllowed{false}; // [v9.0] Strict Startup Delay
358+
356359
// [Fix] Manual Event Queue for Cache Hits (and other internal events)
357360
std::vector<EngineEvent> m_manualEventQueue;
358361
mutable std::mutex m_manualQueueMutex;
362+
363+
// [v9.1] Serial Prefetch Queue
364+
struct PrefetchTask {
365+
int index;
366+
Priority priority;
367+
};
368+
std::deque<PrefetchTask> m_prefetchQueue;
369+
void PumpPrefetch();
359370
public:
360371
bool HasEmbeddedThumb() const { return m_hasEmbeddedThumb.load(); }
361372
};

0 commit comments

Comments
 (0)