Make QuadSurface::gen() safe for concurrent rendering#1075
Open
robert-kofler42 wants to merge 1 commit into
Open
Make QuadSurface::gen() safe for concurrent rendering#1075robert-kofler42 wants to merge 1 commit into
robert-kofler42 wants to merge 1 commit into
Conversation
The renderer (Render.cpp, vc_render_tifxyz) calls QuadSurface::gen() from OpenMP tile workers on a single surface instance. gen() wrote into shared mutable scratch members and lazily built shared caches (_normalCache, _validMaskCache) without synchronization. Concurrently this corrupted output and could crash: one thread's cv::Mat::create() reallocated a buffer another thread was mid-read (the vc-render-tifxyz SIGSEGV, ScrollPrize#1046/ScrollPrize#1054). Fixes: - Move gen()'s coords/normals/validity scratch buffers from shared mutable members to thread_local locals. Preserves the per-frame allocation-reuse optimization (each thread reuses its own buffer) without cross-thread sharing. - Serialize the lazy caches with a new _cacheMutex. validMask() now takes the lock for the whole accessor and returns a header copy made under it. The _normalCache is built *and snapshotted* under the lock, and the expensive warp then runs lock-free on the snapshot. This is the crucial part: plain double-checked locking is not enough, because create() marks the Mat non-empty before it is filled, so a second thread taking the cache-hit path would warp a half-built cache. Every cache-clear site (unloadPoints, unloadCaches, invalidateCache, invalidateMask) takes the same lock. Lock ordering is always _loadMutex -> _cacheMutex. No numeric change: outputs are bit-identical to the serial path. Adds test_quadsurface_gen_concurrency: runs many gen() calls concurrently on one surface and checks the results match a serial reference bit-for-bit. Unfixed it fails 20/20 (corrupted output); fixed it passes (40/40 stress runs, and clean under ThreadSanitizer modulo libgomp's barrier false-positives).
|
Someone is attempting to deploy a commit to the scroll Team on Vercel. A member of the Team first needs to authorize it. |
This was referenced Jun 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The renderer (
Render.cpp,vc_render_tifxyz) callsQuadSurface::gen()from OpenMP tile workers on a single surface instance.gen()wrote into sharedmutablescratch members (_genCoordsScratch/_genNormalsScratch/_genValidScratch) and lazily built shared caches (_normalCache,_validMaskCache) with no synchronization. Concurrently this corrupts output and can crash — one thread'scv::Mat::create()reallocates a buffer another thread is mid-read. This is thevc-render-tifxyzSIGSEGV in #1046, and the thread-safety gap tracked in #1054.Closes #1054. Closes #1046 (same root cause — backtrace points at the
_normalCachewarp ingen(); see analysis on that issue).Fix
thread_local. Each worker reuses its own coords/normals/validity buffer across frames, keeping the per-frame allocation-reuse optimization without cross-thread sharing._cacheMutex.validMask()takes the lock for the whole accessor and returns a header copy made under it._normalCacheis built and snapshotted under the lock; the expensive warp then runs lock-free on the snapshot.cv::Mat::create()marks the Mat non-empty before it is filled — so a second thread on the cache-hit path would warp a half-built cache (silent wrong normals, no crash).unloadPoints,unloadCaches,invalidateCache,invalidateMask) takes the same lock. Lock ordering is always_loadMutex -> _cacheMutex.No numeric change: outputs are bit-identical to the serial path.
Test
Adds
test_quadsurface_gen_concurrency: runs manygen()calls concurrently on one surface and checks each result matches a serial reference bit-for-bit.ThreadSanitizer note: with
-DVC_ENABLE_TSAN=ONthe only remaining warnings are insidevalidMask()'s internal#pragma omp parallel forreduction — a known GCC libgomp false positive (libgomp barriers aren't modeled by TSAN without the archer runtime). Suppress withrace:libgomp.so/called_from_lib:libgomp.so.