fix: use RAII render lock everywhere (#916)

## Summary

Follow-up to
https://github.com/crosspoint-reader/crosspoint-reader/pull/774

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Modernized internal synchronization mechanisms across multiple
components to improve code reliability and maintainability. All
functionality remains unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Xuan-Son Nguyen
2026-02-16 13:53:00 +01:00
committed by GitHub
parent 6702060960
commit 3d47c081f2
7 changed files with 151 additions and 126 deletions

View File

@@ -244,9 +244,10 @@ void WifiSelectionActivity::checkConnectionStatus() {
// Save this as the last connected network - SD card operations need lock as // Save this as the last connected network - SD card operations need lock as
// we use SPI for both // we use SPI for both
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
WIFI_STORE.setLastConnectedSsid(selectedSSID); RenderLock lock(*this);
xSemaphoreGive(renderingMutex); WIFI_STORE.setLastConnectedSsid(selectedSSID);
}
// If we entered a new password, ask if user wants to save it // If we entered a new password, ask if user wants to save it
// Otherwise, immediately complete so parent can start web server // Otherwise, immediately complete so parent can start web server

View File

@@ -221,11 +221,12 @@ void EpubReaderActivity::loop() {
if (skipChapter) { if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
nextPageNumber = 0; RenderLock lock(*this);
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1; nextPageNumber = 0;
section.reset(); currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
xSemaphoreGive(renderingMutex); section.reset();
}
requestUpdate(); requestUpdate();
return; return;
} }
@@ -241,11 +242,12 @@ void EpubReaderActivity::loop() {
section->currentPage--; section->currentPage--;
} else { } else {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
nextPageNumber = UINT16_MAX; RenderLock lock(*this);
currentSpineIndex--; nextPageNumber = UINT16_MAX;
section.reset(); currentSpineIndex--;
xSemaphoreGive(renderingMutex); section.reset();
}
} }
requestUpdate(); requestUpdate();
} else { } else {
@@ -253,11 +255,12 @@ void EpubReaderActivity::loop() {
section->currentPage++; section->currentPage++;
} else { } else {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
nextPageNumber = 0; RenderLock lock(*this);
currentSpineIndex++; nextPageNumber = 0;
section.reset(); currentSpineIndex++;
xSemaphoreGive(renderingMutex); section.reset();
}
} }
requestUpdate(); requestUpdate();
} }
@@ -325,12 +328,13 @@ void EpubReaderActivity::jumpToPercent(int percent) {
} }
// Reset state so render() reloads and repositions on the target spine. // Reset state so render() reloads and repositions on the target spine.
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
currentSpineIndex = targetSpineIndex; RenderLock lock(*this);
nextPageNumber = 0; currentSpineIndex = targetSpineIndex;
pendingPercentJump = true; nextPageNumber = 0;
section.reset(); pendingPercentJump = true;
xSemaphoreGive(renderingMutex); section.reset();
}
} }
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
@@ -403,24 +407,25 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
break; break;
} }
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
if (epub) { RenderLock lock(*this);
// 2. BACKUP: Read current progress if (epub) {
// We use the current variables that track our position // 2. BACKUP: Read current progress
uint16_t backupSpine = currentSpineIndex; // We use the current variables that track our position
uint16_t backupPage = section->currentPage; uint16_t backupSpine = currentSpineIndex;
uint16_t backupPageCount = section->pageCount; uint16_t backupPage = section->currentPage;
uint16_t backupPageCount = section->pageCount;
section.reset(); section.reset();
// 3. WIPE: Clear the cache directory // 3. WIPE: Clear the cache directory
epub->clearCache(); epub->clearCache();
// 4. RESTORE: Re-setup the directory and rewrite the progress file // 4. RESTORE: Re-setup the directory and rewrite the progress file
epub->setupCacheDir(); epub->setupCacheDir();
saveProgress(backupSpine, backupPage, backupPageCount); saveProgress(backupSpine, backupPage, backupPageCount);
}
} }
xSemaphoreGive(renderingMutex);
// Defer go home to avoid race condition with display task // Defer go home to avoid race condition with display task
pendingGoHome = true; pendingGoHome = true;
break; break;
@@ -458,23 +463,24 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
} }
// Preserve current reading position so we can restore after reflow. // Preserve current reading position so we can restore after reflow.
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
if (section) { RenderLock lock(*this);
cachedSpineIndex = currentSpineIndex; if (section) {
cachedChapterTotalPageCount = section->pageCount; cachedSpineIndex = currentSpineIndex;
nextPageNumber = section->currentPage; cachedChapterTotalPageCount = section->pageCount;
nextPageNumber = section->currentPage;
}
// Persist the selection so the reader keeps the new orientation on next launch.
SETTINGS.orientation = orientation;
SETTINGS.saveToFile();
// Update renderer orientation to match the new logical coordinate system.
applyReaderOrientation(renderer, SETTINGS.orientation);
// Reset section to force re-layout in the new orientation.
section.reset();
} }
// Persist the selection so the reader keeps the new orientation on next launch.
SETTINGS.orientation = orientation;
SETTINGS.saveToFile();
// Update renderer orientation to match the new logical coordinate system.
applyReaderOrientation(renderer, SETTINGS.orientation);
// Reset section to force re-layout in the new orientation.
section.reset();
xSemaphoreGive(renderingMutex);
} }
// TODO: Failure handling // TODO: Failure handling

View File

@@ -51,18 +51,20 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
LOG_DBG("KOSync", "WiFi connected, starting sync"); LOG_DBG("KOSync", "WiFi connected, starting sync");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNCING; RenderLock lock(*this);
statusMessage = "Syncing time..."; state = SYNCING;
xSemaphoreGive(renderingMutex); statusMessage = "Syncing time...";
}
requestUpdate(); requestUpdate();
// Sync time with NTP before making API requests // Sync time with NTP before making API requests
syncTimeWithNTP(); syncTimeWithNTP();
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
statusMessage = "Calculating document hash..."; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); statusMessage = "Calculating document hash...";
}
requestUpdate(); requestUpdate();
performSync(); performSync();
@@ -76,19 +78,21 @@ void KOReaderSyncActivity::performSync() {
documentHash = KOReaderDocumentId::calculate(epubPath); documentHash = KOReaderDocumentId::calculate(epubPath);
} }
if (documentHash.empty()) { if (documentHash.empty()) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNC_FAILED; RenderLock lock(*this);
statusMessage = "Failed to calculate document hash"; state = SYNC_FAILED;
xSemaphoreGive(renderingMutex); statusMessage = "Failed to calculate document hash";
}
requestUpdate(); requestUpdate();
return; return;
} }
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str()); LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
statusMessage = "Fetching remote progress..."; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); statusMessage = "Fetching remote progress...";
}
requestUpdateAndWait(); requestUpdateAndWait();
// Fetch remote progress // Fetch remote progress
@@ -96,19 +100,21 @@ void KOReaderSyncActivity::performSync() {
if (result == KOReaderSyncClient::NOT_FOUND) { if (result == KOReaderSyncClient::NOT_FOUND) {
// No remote progress - offer to upload // No remote progress - offer to upload
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = NO_REMOTE_PROGRESS; RenderLock lock(*this);
hasRemoteProgress = false; state = NO_REMOTE_PROGRESS;
xSemaphoreGive(renderingMutex); hasRemoteProgress = false;
}
requestUpdate(); requestUpdate();
return; return;
} }
if (result != KOReaderSyncClient::OK) { if (result != KOReaderSyncClient::OK) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNC_FAILED; RenderLock lock(*this);
statusMessage = KOReaderSyncClient::errorString(result); state = SYNC_FAILED;
xSemaphoreGive(renderingMutex); statusMessage = KOReaderSyncClient::errorString(result);
}
requestUpdate(); requestUpdate();
return; return;
} }
@@ -122,18 +128,20 @@ void KOReaderSyncActivity::performSync() {
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine}; CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
localProgress = ProgressMapper::toKOReader(epub, localPos); localProgress = ProgressMapper::toKOReader(epub, localPos);
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SHOWING_RESULT; RenderLock lock(*this);
selectedOption = 0; // Default to "Apply" state = SHOWING_RESULT;
xSemaphoreGive(renderingMutex); selectedOption = 0; // Default to "Apply"
}
requestUpdate(); requestUpdate();
} }
void KOReaderSyncActivity::performUpload() { void KOReaderSyncActivity::performUpload() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = UPLOADING; RenderLock lock(*this);
statusMessage = "Uploading progress..."; state = UPLOADING;
xSemaphoreGive(renderingMutex); statusMessage = "Uploading progress...";
}
requestUpdate(); requestUpdate();
requestUpdateAndWait(); requestUpdateAndWait();
@@ -149,17 +157,19 @@ void KOReaderSyncActivity::performUpload() {
const auto result = KOReaderSyncClient::updateProgress(progress); const auto result = KOReaderSyncClient::updateProgress(progress);
if (result != KOReaderSyncClient::OK) { if (result != KOReaderSyncClient::OK) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNC_FAILED; RenderLock lock(*this);
statusMessage = KOReaderSyncClient::errorString(result); state = SYNC_FAILED;
xSemaphoreGive(renderingMutex); statusMessage = KOReaderSyncClient::errorString(result);
}
requestUpdate(); requestUpdate();
return; return;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = UPLOAD_COMPLETE; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = UPLOAD_COMPLETE;
}
requestUpdate(); requestUpdate();
} }
@@ -190,9 +200,10 @@ void KOReaderSyncActivity::onEnter() {
auto* self = static_cast<KOReaderSyncActivity*>(param); auto* self = static_cast<KOReaderSyncActivity*>(param);
// Sync time first // Sync time first
syncTimeWithNTP(); syncTimeWithNTP();
xSemaphoreTake(self->renderingMutex, portMAX_DELAY); {
self->statusMessage = "Calculating document hash..."; RenderLock lock(*self);
xSemaphoreGive(self->renderingMutex); self->statusMessage = "Calculating document hash...";
}
self->requestUpdate(); self->requestUpdate();
self->performSync(); self->performSync();
vTaskDelete(nullptr); vTaskDelete(nullptr);

View File

@@ -118,9 +118,10 @@ void ClearCacheActivity::loop() {
if (state == WARNING) { if (state == WARNING) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear"); LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = CLEARING; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = CLEARING;
}
requestUpdateAndWait(); requestUpdateAndWait();
clearCache(); clearCache();

View File

@@ -14,18 +14,20 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
exitActivity(); exitActivity();
if (!success) { if (!success) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = FAILED; RenderLock lock(*this);
errorMessage = "WiFi connection failed"; state = FAILED;
xSemaphoreGive(renderingMutex); errorMessage = "WiFi connection failed";
}
requestUpdate(); requestUpdate();
return; return;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = AUTHENTICATING; RenderLock lock(*this);
statusMessage = "Authenticating..."; state = AUTHENTICATING;
xSemaphoreGive(renderingMutex); statusMessage = "Authenticating...";
}
requestUpdate(); requestUpdate();
performAuthentication(); performAuthentication();
@@ -34,15 +36,16 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
void KOReaderAuthActivity::performAuthentication() { void KOReaderAuthActivity::performAuthentication() {
const auto result = KOReaderSyncClient::authenticate(); const auto result = KOReaderSyncClient::authenticate();
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
if (result == KOReaderSyncClient::OK) { RenderLock lock(*this);
state = SUCCESS; if (result == KOReaderSyncClient::OK) {
statusMessage = "Successfully authenticated!"; state = SUCCESS;
} else { statusMessage = "Successfully authenticated!";
state = FAILED; } else {
errorMessage = KOReaderSyncClient::errorString(result); state = FAILED;
errorMessage = KOReaderSyncClient::errorString(result);
}
} }
xSemaphoreGive(renderingMutex);
requestUpdate(); requestUpdate();
} }

View File

@@ -121,7 +121,6 @@ void KOReaderSettingsActivity::handleSelection() {
// Authenticate // Authenticate
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
// Can't authenticate without credentials - just show message briefly // Can't authenticate without credentials - just show message briefly
xSemaphoreGive(renderingMutex);
return; return;
} }
exitActivity(); exitActivity();

View File

@@ -20,33 +20,37 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
LOG_DBG("OTA", "WiFi connected, checking for update"); LOG_DBG("OTA", "WiFi connected, checking for update");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = CHECKING_FOR_UPDATE; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = CHECKING_FOR_UPDATE;
}
requestUpdateAndWait(); requestUpdateAndWait();
const auto res = updater.checkForUpdate(); const auto res = updater.checkForUpdate();
if (res != OtaUpdater::OK) { if (res != OtaUpdater::OK) {
LOG_DBG("OTA", "Update check failed: %d", res); LOG_DBG("OTA", "Update check failed: %d", res);
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = FAILED; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = FAILED;
}
requestUpdate(); requestUpdate();
return; return;
} }
if (!updater.isUpdateNewer()) { if (!updater.isUpdateNewer()) {
LOG_DBG("OTA", "No new update available"); LOG_DBG("OTA", "No new update available");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = NO_UPDATE; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = NO_UPDATE;
}
requestUpdate(); requestUpdate();
return; return;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = WAITING_CONFIRMATION; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = WAITING_CONFIRMATION;
}
requestUpdate(); requestUpdate();
} }