Skip to content

Commit 6ce4102

Browse files
author
Grok Compression
committed
Fix deadlock in band-callback backpressure for reduced images
The incremental band-write callback's empty-row handler used a strict in-order check (tileY == nextBandTileY_) with an early return. When Taskflow completed tile rows out of order — common with tiny tiles from high resolution reduction (-r 4 on a 5-resolution image) — empty rows finishing before prior rows silently lost their nextBandTileY_ advancement. This left the parser thread's backpressure loop (bandDrainCV_.wait_for) spinning forever, blocking decompressWorker_ and causing the main thread to hang on join(). Replace the early-return path with sentinel entries in pendingBands_. The existing drain loop now processes empty rows in order alongside non-empty ones, skipping compositing and the band callback but still advancing nextBandTileY_ and waking the parser thread. This handles out-of-order row completion correctly regardless of scheduling order. Reproduces ~30-50% of the time on p1_06.j2k with -r 4; verified fixed over 150 consecutive iterations with zero hangs.
1 parent 43b1d6a commit 6ce4102

1 file changed

Lines changed: 41 additions & 43 deletions

File tree

src/lib/core/codestream/decompress/CodeStreamDecompress.cpp

Lines changed: 41 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -258,73 +258,71 @@ bool CodeStreamDecompress::decompress(grk_plugin_tile* tile)
258258
// for the current tile row
259259
uint32_t yBegin = 0;
260260
uint32_t yEnd = std::min(tileGlobalYEnd, regionY1) - std::max(tileGlobalYBegin, regionY0);
261-
if(yEnd <= yBegin)
262-
{
263-
// Empty tile row (e.g. zero pixels after resolution reduction).
264-
// Still advance nextBandTileY_ so backpressure unblocks.
265-
std::lock_guard<std::mutex> lock(bandOrderMutex_);
266-
if(tileY == nextBandTileY_)
267-
nextBandTileY_ = tileY + 1;
268-
bandDrainCV_.notify_one();
269-
return;
270-
}
271261

272262
uint16_t tileX0 = tileIndexBegin % numTileCols;
273263
uint16_t numSlatedCols = (uint16_t)tilesToDecompress_.getSlatedTileRect().width();
274264

275265
// All compositing, band writing, and strip advancing must be serialized
276266
// to prevent races on the shared strip buffer.
267+
// Empty rows (yEnd <= yBegin) are inserted as sentinels so the drain
268+
// loop processes them in order — an early-return would lose the
269+
// advancement when an empty row completes before a prior row.
277270
std::lock_guard<std::mutex> lock(bandOrderMutex_);
278271
pendingBands_[tileY] = {yBegin, yEnd, tileX0, numSlatedCols};
279272
while(pendingBands_.count(nextBandTileY_))
280273
{
281274
auto& band = pendingBands_[nextBandTileY_];
282275

283-
// Check if any tile in this row already wrote strip output directly
284-
bool stripOutputHandled = false;
285-
for(uint16_t col = 0; col < band.numCols; col++)
286-
{
287-
uint16_t tileIndex =
288-
static_cast<uint16_t>(nextBandTileY_ * numTileCols + (band.tileX0 + col));
289-
auto cacheEntry = tileCache_->get(tileIndex);
290-
if(cacheEntry && cacheEntry->processor &&
291-
cacheEntry->processor->isStripOutputWritten())
292-
{
293-
stripOutputHandled = true;
294-
break;
295-
}
296-
}
297-
298-
if(!stripOutputHandled)
276+
// Skip compositing and callback for empty rows (zero pixels
277+
// after resolution reduction).
278+
if(band.yEnd > band.yBegin)
299279
{
300-
// Composite all tiles in this row into the strip buffer
280+
// Check if any tile in this row already wrote strip output directly
281+
bool stripOutputHandled = false;
301282
for(uint16_t col = 0; col < band.numCols; col++)
302283
{
303284
uint16_t tileIndex =
304285
static_cast<uint16_t>(nextBandTileY_ * numTileCols + (band.tileX0 + col));
305286
auto cacheEntry = tileCache_->get(tileIndex);
306-
if(!cacheEntry || !cacheEntry->processor)
307-
continue;
308-
auto tileImage = cacheEntry->processor->getImage();
309-
if(tileImage)
287+
if(cacheEntry && cacheEntry->processor &&
288+
cacheEntry->processor->isStripOutputWritten())
310289
{
311-
if(!scratchImage_->composite(tileImage))
312-
success_ = false;
290+
stripOutputHandled = true;
291+
break;
313292
}
314293
}
315294

316-
if(!ioBandCallback_(band.yBegin, band.yEnd, scratchImage_.get(), ioBandUserData_))
317-
success_ = false;
318-
}
295+
if(!stripOutputHandled)
296+
{
297+
// Composite all tiles in this row into the strip buffer
298+
for(uint16_t col = 0; col < band.numCols; col++)
299+
{
300+
uint16_t tileIndex =
301+
static_cast<uint16_t>(nextBandTileY_ * numTileCols + (band.tileX0 + col));
302+
auto cacheEntry = tileCache_->get(tileIndex);
303+
if(!cacheEntry || !cacheEntry->processor)
304+
continue;
305+
auto tileImage = cacheEntry->processor->getImage();
306+
if(tileImage)
307+
{
308+
if(!scratchImage_->composite(tileImage))
309+
success_ = false;
310+
}
311+
}
319312

320-
// Release tile processors for this completed row
321-
for(uint16_t col = 0; col < band.numCols; col++)
322-
{
323-
uint16_t tileIndex =
324-
static_cast<uint16_t>(nextBandTileY_ * numTileCols + (band.tileX0 + col));
325-
tileCache_->releaseForSwath(tileIndex);
313+
if(!ioBandCallback_(band.yBegin, band.yEnd, scratchImage_.get(), ioBandUserData_))
314+
success_ = false;
315+
}
316+
317+
// Release tile processors for this completed row
318+
for(uint16_t col = 0; col < band.numCols; col++)
319+
{
320+
uint16_t tileIndex =
321+
static_cast<uint16_t>(nextBandTileY_ * numTileCols + (band.tileX0 + col));
322+
tileCache_->releaseForSwath(tileIndex);
323+
}
324+
MemoryManager::releaseFreedPages();
326325
}
327-
MemoryManager::releaseFreedPages();
328326

329327
pendingBands_.erase(nextBandTileY_);
330328

0 commit comments

Comments
 (0)