• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saitoha / libsixel / 19715072951

26 Nov 2025 07:14PM UTC coverage: 41.196% (-0.03%) from 41.224%
19715072951

push

github

saitoha
rename sixel_parallel_logger_t -> sixel_logger_t

9693 of 33860 branches covered (28.63%)

52 of 126 new or added lines in 8 files covered. (41.27%)

4 existing lines in 1 file now uncovered.

12858 of 31212 relevant lines covered (41.2%)

666536.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.01
/src/decoder-parallel.c
1
/*
2
 * SPDX-License-Identifier: MIT
3
 *
4
 * Copyright (c) 2025 libsixel developers. See `AUTHORS`.
5
 */
6

7
#include "config.h"
8

9
#include <errno.h>
10
#include <limits.h>
11
#include <stdlib.h>
12
#include <string.h>
13

14
#include <sixel.h>
15

16
#include "decoder-parallel.h"
17
#if SIXEL_ENABLE_THREADS
18
# include "logger.h"
19
# include "sixel_threading.h"
20
#endif
21

22
/*
23
 * The previous prescan-based parallel decoder has been removed.
24
 * The current implementation keeps the public entry points so that
25
 * callers can fall back to the single threaded decoder while we
26
 * prepare the new chunked worker pipeline.
27
 */
28

29
typedef struct sixel_decoder_thread_config {
30
    int env_checked;
31
    int env_valid;
32
    int env_threads;
33
    int override_active;
34
    int override_threads;
35
} sixel_decoder_thread_config_t;
36

37
#if SIXEL_ENABLE_THREADS
38
typedef struct sixel_decoder_worker_context {
39
    struct sixel_decoder_worker_chain *chain;
40
    unsigned char *input;
41
    unsigned char *anchor;
42
    int length;
43
    int payload_len;
44
    int start_offset;
45
    int end_offset;
46
    int index;
47
    int direct_mode;
48
    int const *palette;
49
    int palette_limit;
50
    int width;
51
    int height;
52
    int pixel_size;
53
    int depth;
54
    sixel_logger_t *logger;
55
    unsigned char *local_buffer;
56
    int local_capacity;
57
    int local_written;
58
    int result;
59
} sixel_decoder_worker_context_t;
60

61
typedef struct sixel_decoder_local_chunk {
62
    unsigned char *data;
63
    int start_row;
64
    int rows;
65
    struct sixel_decoder_local_chunk *next;
66
} sixel_decoder_local_chunk_t;
67

68
typedef struct sixel_local_buffer {
69
    sixel_decoder_local_chunk_t *head;
70
    sixel_decoder_local_chunk_t *tail;
71
    sixel_decoder_local_chunk_t *cursor;
72
    int width;
73
    int pixel_size;
74
} sixel_local_buffer_t;
75

76
typedef struct sixel_decoder_worker_chain {
77
    sixel_mutex_t mutex;
78
    sixel_cond_t cond;
79
    int *copy_offsets;
80
    int *copy_ready;
81
    int thread_count;
82
    unsigned char *global_buffer;
83
    int global_capacity;
84
    int pixel_size;
85
    int abort_requested;
86
} sixel_decoder_worker_chain_t;
87
#endif
88

89
static sixel_decoder_thread_config_t g_decoder_threads = {
90
    0,
91
    0,
92
    1,
93
    0,
94
    1
95
};
96

97
#if SIXEL_ENABLE_THREADS
98
static void
99
sixel_decoder_parallel_log_stub(sixel_logger_t *logger,
226✔
100
                                char const *mode)
101
{
102
    /*
103
     * Emit a minimal timeline so SIXEL_PARALLEL_LOG_PATH mirrors the
104
     * encoder behaviour even while the decoder still falls back to the
105
     * serial path.
106
     */
107
    if (logger == NULL) {
226!
108
        return;
×
109
    }
110

111
    sixel_logger_logf(logger,
302✔
112
                      "decoder",
113
                      mode,
76✔
114
                      "start",
115
                      0,
116
                      0,
117
                      0,
118
                      0,
119
                      0,
120
                      0,
121
                      "parallel decoder stub active");
122
    sixel_logger_logf(logger,
302✔
123
                      "decoder",
124
                      mode,
76✔
125
                      "fallback",
126
                      0,
127
                      0,
128
                      0,
129
                      0,
130
                      0,
131
                      0,
132
                      "falling back to serial implementation");
133
}
76✔
134

135
static void
136
sixel_decoder_parallel_fill_spans(int payload_len,
137
                                  int threads,
138
                                  int *spans);
139

140
static int
141
sixel_decoder_parallel_skew_percent(void)
18✔
142
{
143
    char const *text;
144
    char *endptr;
145
    long value;
146

147
    /*
148
     * SIXEL_PARALLEL_SKEW lets operators bias span lengths by +/-20% so the
149
     * trailing workers take slightly more work.  The default keeps spans
150
     * balanced.
151
     */
152
    text = getenv("SIXEL_PARALLEL_SKEW");
18✔
153
    if (text == NULL || text[0] == '\0') {
18!
154
        return 0;
18✔
155
    }
156

157
    errno = 0;
×
158
    value = strtol(text, &endptr, 10);
×
159
    if (errno != 0 || endptr == text || *endptr != '\0') {
×
160
        return 0;
×
161
    }
162

163
    if (value < -20) {
×
164
        value = -20;
×
165
    } else if (value > 20) {
×
166
        value = 20;
×
167
    }
168

169
    return (int)value;
×
170
}
6✔
171

172
static void
173
sixel_decoder_parallel_fill_spans(int payload_len,
18✔
174
                                  int threads,
175
                                  int *spans)
176
{
177
    int base_share;
178
    int skew_percent;
179
    double skew;
180
    int i;
181
    double center;
182
    int total;
183
    int remainder;
184

185
    base_share = payload_len / threads;
18✔
186
    skew_percent = sixel_decoder_parallel_skew_percent();
18✔
187
    skew = ((double)base_share * (double)skew_percent) / 100.0;
18✔
188
    total = 0;
18✔
189
    for (i = 0; i < threads; ++i) {
90✔
190
        center = (double)i - (double)(threads - 1) / 2.0;
72✔
191
        spans[i] = base_share + (int)(skew * center);
72✔
192
        if (spans[i] < 1) {
72!
193
            spans[i] = 1;
×
194
        }
195
        total += spans[i];
72✔
196
    }
24✔
197

198
    remainder = payload_len - total;
18✔
199
    while (remainder > 0) {
24✔
200
        for (i = threads - 1; i >= 0 && remainder > 0; --i) {
12!
201
            spans[i] += 1;
6✔
202
            --remainder;
6✔
203
        }
2✔
204
    }
205
    while (remainder < 0) {
18!
206
        for (i = threads - 1; i >= 0 && remainder < 0; --i) {
×
207
            if (spans[i] > 1) {
×
208
                spans[i] -= 1;
×
209
                ++remainder;
×
210
            }
211
        }
212
        if (remainder < 0) {
×
213
            break;
×
214
        }
215
    }
216
}
18✔
217

218
static int
219
sixel_decoder_parallel_pixel_size(int depth)
223✔
220
{
221
    int pixel_size;
222

223
    pixel_size = 4;
223✔
224
    if (depth == 1) {
223✔
225
        pixel_size = 1;
205✔
226
    } else if (depth == 2) {
87!
227
        pixel_size = 2;
×
228
    }
229

230
    return pixel_size;
223✔
231
}
232

233
static void
234
sixel_decoder_parallel_store_pixel(unsigned char *dst,
1,551,122✔
235
                                   int depth,
236
                                   int color_index,
237
                                   int const *palette)
238
{
239
    int color;
240

241
    if (depth == 1) {
1,551,122✔
242
        dst[0] = (unsigned char)color_index;
768,442✔
243
        return;
768,442✔
244
    }
245

246
    if (depth == 2) {
782,680!
247
        unsigned short packed;
248

249
        packed = (unsigned short)color_index;
×
250
        memcpy(dst, &packed, sizeof(packed));
×
251
        return;
×
252
    }
253

254
    color = 0;
782,680✔
255
    if (palette != NULL &&
1,024,156!
256
            color_index >= 0 &&
782,437!
257
            color_index < SIXEL_PALETTE_MAX_DECODER) {
241,476✔
258
        color = palette[color_index];
782,756✔
259
    }
241,604✔
260
    dst[0] = (unsigned char)((color >> 16) & 0xff);
783,318✔
261
    dst[1] = (unsigned char)((color >> 8) & 0xff);
783,318✔
262
    dst[2] = (unsigned char)(color & 0xff);
783,318✔
263
    dst[3] = 255u;
783,318✔
264
}
489,716✔
265

266
static void
267
sixel_local_buffer_init(sixel_local_buffer_t *buffer,
72✔
268
                        int width,
269
                        int pixel_size)
270
{
271
    buffer->head = NULL;
72✔
272
    buffer->tail = NULL;
72✔
273
    buffer->cursor = NULL;
72✔
274
    buffer->width = width;
72✔
275
    buffer->pixel_size = pixel_size;
72✔
276
}
72✔
277

278
static void
279
sixel_local_buffer_dispose(sixel_local_buffer_t *buffer)
72✔
280
{
281
    sixel_decoder_local_chunk_t *cursor;
282

283
    cursor = buffer->head;
72✔
284
    while (cursor != NULL) {
150✔
285
        sixel_decoder_local_chunk_t *tmp;
286

287
        free(cursor->data);
78✔
288
        tmp = cursor->next;
78✔
289
        free(cursor);
78✔
290
        cursor = tmp;
78✔
291
    }
292

293
    buffer->head = NULL;
72✔
294
    buffer->tail = NULL;
72✔
295
    buffer->cursor = NULL;
72✔
296
}
72✔
297

298
static sixel_decoder_local_chunk_t *
299
sixel_local_buffer_append(sixel_local_buffer_t *buffer,
78✔
300
                          int rows,
301
                          int start_row)
302
{
303
    sixel_decoder_local_chunk_t *chunk;
304
    size_t bytes;
305

306
    if (rows % 6 != 0) {
78!
307
        rows += 6 - (rows % 6);
×
308
    }
309

310
    chunk = (sixel_decoder_local_chunk_t *)calloc(
78✔
311
        1, sizeof(sixel_decoder_local_chunk_t));
312
    if (chunk == NULL) {
78!
313
        return NULL;
×
314
    }
315

316
    bytes = (size_t)(rows * buffer->width) * (size_t)buffer->pixel_size;
78✔
317
    chunk->data = (unsigned char *)calloc(bytes, 1);
78✔
318
    if (chunk->data == NULL) {
78!
319
        free(chunk);
×
320
        return NULL;
×
321
    }
322

323
    chunk->start_row = start_row;
78✔
324
    chunk->rows = rows;
78✔
325
    chunk->next = NULL;
78✔
326

327
    if (buffer->head == NULL) {
78✔
328
        buffer->head = chunk;
72✔
329
    } else {
24✔
330
        buffer->tail->next = chunk;
6✔
331
    }
332
    buffer->tail = chunk;
78✔
333
    buffer->cursor = chunk;
78✔
334

335
    return chunk;
78✔
336
}
26✔
337

338
static sixel_decoder_local_chunk_t *
339
sixel_local_buffer_reserve_row(sixel_local_buffer_t *buffer,
1,529,132✔
340
                               int row)
341
{
342
    sixel_decoder_local_chunk_t *cursor;
343

344
    cursor = buffer->cursor;
1,529,132✔
345
    while (cursor != NULL && row >= cursor->start_row + cursor->rows) {
1,529,138✔
346
        cursor = cursor->next;
6✔
347
    }
348
    if (cursor != NULL && row >= cursor->start_row) {
1,526,026!
349
        buffer->cursor = cursor;
1,526,024✔
350
        return cursor;
1,526,024✔
351
    }
352

353
    cursor = buffer->tail;
1,192✔
354
    if (cursor == NULL) {
1,192!
355
        return NULL;
×
356
    }
357

358
    while (row >= cursor->start_row + cursor->rows) {
12✔
359
        sixel_decoder_local_chunk_t *next;
360
        int start_row;
361

362
        start_row = cursor->start_row + cursor->rows;
6✔
363
        next = sixel_local_buffer_append(buffer, cursor->rows, start_row);
6✔
364
        if (next == NULL) {
6!
365
            return NULL;
×
366
        }
367
        cursor = next;
6✔
368
    }
369

370
    buffer->cursor = cursor;
6✔
371
    return cursor;
6✔
372
}
477,838✔
373

374
/*
375
 * Worker entry for the fast sixel parser.  Each thread jumps to the next
376
 * '-' marker from its assigned offset, then walks tokens until the first
377
 * '-' after its scheduled end offset.
378
 *
379
 * Supported tokens: palette switches ('#n'), carriage return ('$'), next
380
 * line ('-'), and raster data ('?' - '~').  Any raster attribute reset '"',
381
 * palette redefinition '#n;type;...', or CAN/SUB control code marks the
382
 * chunk as invalid and returns a failure status so the caller can fall back.
383
 */
384
static int
385
sixel_decoder_parallel_worker(void *arg)
11,904✔
386
{
387
    sixel_decoder_worker_context_t *context;
388
    sixel_decoder_worker_chain_t *chain;
389
    sixel_local_buffer_t local_buffer;
390
    sixel_decoder_local_chunk_t *chunk_cursor = NULL;
11,904✔
391
    unsigned char *anchor = NULL;
11,904✔
392
    unsigned char *scan = NULL;
11,904✔
393
    unsigned char *cursor = NULL;
11,904✔
394
    unsigned char *stop = NULL;
11,904✔
395
    unsigned char *limit = NULL;
11,904✔
396
    unsigned char *start = NULL;
11,904✔
397
    int capacity = 0;
11,904✔
398
    int written = 0;
11,904✔
399
    int assigned = 0;
11,904✔
400
    int bits = 0;
11,904✔
401
    int repeat = 1;
11,904✔
402
    int color_index = 0;
11,904✔
403
    int max_relative = (-1);
11,904✔
404
    int min_relative = (-1);
11,904✔
405
    int pos_x = 0;
11,904✔
406
    int pos_y = 0;
11,904✔
407
    int relative = 0;
11,904✔
408
    int row_base = 0;
11,904✔
409
    int r = 0;
11,904✔
410
    int row_offset = 0;
11,904✔
411
    int line_count = 0;
11,904✔
412
    int copy_offset = 0;
11,904✔
413
    int copy_span = 0;
11,904✔
414
    int next_offset = 0;
11,904✔
415
    int i;
416
    int fallback = 0;
11,904✔
417
    int status = (-1);
11,904✔
418
    sixel_logger_t *logger = NULL;
11,904✔
419
    int width = 0;
11,904✔
420
    int pixel_size = 0;
11,904✔
421
    int depth = 0;
11,904✔
422
    int height = 0;
11,904✔
423
    int chain_offset = 0;
11,904✔
424
    unsigned char ch;
425

426
    context = (sixel_decoder_worker_context_t *)arg;
11,904✔
427
    if (context == NULL) {
11,904!
428
        return (-1);
×
429
    }
430

431
    context->result = status;
11,904✔
432

433
    chain = context->chain;
11,904✔
434
    if (chain == NULL) {
11,904!
435
        context->result = status;
×
436
        return status;
×
437
    }
438

439
    logger = context->logger;
11,904✔
440
    anchor = context->anchor;
11,904✔
441
    if (context->input == NULL || context->length <= 0) {
11,904!
442
        context->result = status;
23,664✔
443
        return status;
23,664✔
444
    }
445

446
    width = context->width;
11,904✔
447
    if (width <= 0) {
11,904!
448
        context->result = status;
×
449
        return status;
×
450
    }
451

452
    pixel_size = context->pixel_size;
11,904✔
453
    depth = context->depth;
11,904✔
454
    height = context->height;
11,904✔
455
    if (height <= 0) {
11,904!
456
        context->result = status;
×
457
        return status;
×
458
    }
459
    sixel_local_buffer_init(&local_buffer, width, pixel_size);
11,904✔
460
    if (context->payload_len <= 0) {
11,904!
461
        context->result = status;
×
462
        return status;
×
463
    }
464

465
    start = context->input + context->start_offset;
11,904✔
466
    if (start < context->input ||
11,904!
467
            start >= context->input + context->length) {
72!
468
        context->result = status;
23,664✔
469
        return status;
23,664✔
470
    }
471

472
    assigned = context->end_offset - context->start_offset + 1;
11,904✔
473
    if (assigned <= 0) {
11,904!
474
        context->result = status;
×
475
        return status;
×
476
    }
477

478
    status = 0;
11,904✔
479

480
    if (logger != NULL) {
11,904!
481
        /*
482
         * Decode window for this worker. The key keeps decode spans grouped
483
         * per-thread in the timeline output.
484
         */
NEW
485
        sixel_logger_logf(logger,
×
486
                          "decode",
487
                          "decoder",
488
                          "start",
489
                          context->index,
490
                          context->index,
491
                          0,
492
                          0,
493
                          context->start_offset,
494
                          context->end_offset,
495
                          "worker %d decode span [%d,%d]",
496
                          context->index,
497
                          context->start_offset,
498
                          context->end_offset);
499
    }
500

501
    cursor = start;
11,904✔
502
    if (context->index > 0) {
11,904✔
503
        cursor = (unsigned char *)memchr(start,
72✔
504
                                         '-',
505
                                         (size_t)(context->length -
72✔
506
                                         context->start_offset));
54✔
507
        if (cursor != NULL &&
54!
508
                cursor + 1 < context->input + context->length) {
54!
509
            cursor += 1;
54✔
510
        } else {
18✔
511
            cursor = start;
×
512
        }
513
    }
18✔
514
    if (context->index > 0 && cursor == start) {
11,904!
515
        status = (-1);
×
516
        context->result = status;
×
517
        return status;
×
518
    }
519

520
    if (anchor != NULL && anchor < cursor) {
11,904!
521
        scan = anchor;
78✔
522
        while (scan < cursor) {
2,094,306✔
523
            if (*scan == '-') {
2,094,252✔
524
                line_count += 1;
750✔
525
                scan += 1;
750✔
526
                continue;
750✔
527
            }
528

529
            if (*scan == '#') {
2,093,502✔
530
                int value;
531
                unsigned char *p;
532

533
                value = 0;
139,613✔
534
                p = scan + 1;
139,613✔
535
                while (p < cursor && *p >= '0' && *p <= '9') {
431,455!
536
                    value = value * 10 + (*p - '0');
291,842✔
537
                    p += 1;
291,842✔
538
                }
539
                color_index = value;
139,613✔
540
                if (color_index < 0) {
139,613!
541
                    color_index = 0;
×
542
                }
543
                if (color_index >= SIXEL_PALETTE_MAX_DECODER) {
139,613!
544
                    color_index = SIXEL_PALETTE_MAX_DECODER - 1;
×
545
                }
546
                if (p < cursor && *p == ';') {
139,613!
547
                    scan = p;
×
548
                    continue;
×
549
                }
550
                scan = p;
139,613✔
551
                continue;
139,613✔
552
            }
553

554
            scan += 1;
1,953,889✔
555
        }
556
    }
18✔
557
    row_offset = line_count * 6;
11,916✔
558

559
    capacity = (int)((double)chain->global_capacity *
23,784✔
560
        ((double)assigned / (double)context->payload_len));
11,916✔
561
    capacity = (int)((double)capacity * 1.10);
11,916✔
562
    if (capacity < 1) {
11,916!
563
        capacity = 1;
×
564
    }
565
    if (capacity < width * 6) {
11,916✔
566
        capacity = width * 6;
48✔
567
    }
16✔
568
    if (capacity % (width * 6) != 0) {
11,916✔
569
        capacity += (width * 6) - (capacity % (width * 6));
24✔
570
    }
8✔
571

572
    chunk_cursor = sixel_local_buffer_append(&local_buffer,
11,916✔
573
                                             capacity / width,
11,868✔
574
                                             0);
575
    if (chunk_cursor == NULL) {
11,916!
576
        status = (-1);
×
577
        context->result = status;
×
578
        return status;
×
579
    }
580
    capacity = chunk_cursor->rows * width;
11,916✔
581

582
    stop = context->input + context->end_offset;
11,916✔
583
    limit = context->input + context->length;
11,916✔
584
    while (cursor < limit) {
1,250,492!
585
        ch = *cursor;
1,250,492✔
586

587
        if (ch < 0x20) {
1,250,492✔
588
            if (ch == 0x18 || ch == 0x1a) {
186!
589
                fallback = 1;
×
590
                status = (-1);
×
591
                break;
×
592
            }
593
            cursor += 1;
186✔
594
            if (ch == 0x1b && cursor < limit && *cursor == '\\') {
186!
595
                status = (0);
42✔
596
                break;
42✔
597
            }
598
            continue;
144✔
599
        }
600

601
        if (ch == '"') {
1,250,306!
602
            fallback = 1;
×
603
            status = (-1);
×
604
            break;
×
605
        }
606

607
        if (ch == '!') {
1,250,306✔
608
            int value;
609
            unsigned char *p;
610

611
            value = 0;
69,861✔
612
            p = cursor + 1;
69,861✔
613
            while (p < limit && *p >= '0' && *p <= '9') {
167,351!
614
                value = value * 10 + (*p - '0');
97,490✔
615
                p += 1;
97,490✔
616
            }
617
            if (value <= 0) {
69,861!
618
                value = 1;
×
619
            }
620
            repeat = value;
69,861✔
621
            cursor = p;
69,861✔
622
            continue;
69,861✔
623
        }
624

625
        if (ch == '#') {
1,180,445✔
626
            int value;
627
            unsigned char *p;
628

629
            value = 0;
91,399✔
630
            p = cursor + 1;
91,399✔
631
            while (p < limit && *p >= '0' && *p <= '9') {
291,178!
632
                value = value * 10 + (*p - '0');
199,779✔
633
                p += 1;
199,779✔
634
            }
635
            if (p < limit && *p == ';') {
91,399!
636
                fallback = 1;
×
637
                status = (-1);
×
638
                break;
×
639
            }
640
            color_index = value;
91,399✔
641
            if (color_index < 0) {
91,399!
642
                color_index = 0;
×
643
            }
644
            if (color_index >= SIXEL_PALETTE_MAX_DECODER) {
91,195!
645
                color_index = SIXEL_PALETTE_MAX_DECODER - 1;
×
646
            }
647
            cursor = p;
91,195✔
648
            continue;
91,195✔
649
        }
650

651
        if (ch == '$') {
1,089,046✔
652
            cursor += 1;
4,751✔
653
            pos_x = 0;
4,751✔
654
            continue;
4,751✔
655
        }
656

657
        if (ch == '-') {
1,084,295✔
658
            if (cursor >= stop) {
444✔
659
                break;
18✔
660
            }
661
            cursor += 1;
426✔
662
            pos_x = 0;
426✔
663
            pos_y += 6;
426✔
664
            chunk_cursor = sixel_local_buffer_reserve_row(&local_buffer,
426✔
665
                                                          pos_y);
142✔
666
            if (chunk_cursor == NULL) {
426!
667
                fallback = 1;
×
668
                status = (-1);
×
669
                break;
×
670
            }
671
            continue;
426✔
672
        }
673

674
        if (ch < '?' || ch > '~') {
1,083,851!
675
            cursor += 1;
22,908✔
676
            continue;
22,908✔
677
        }
678

679
        bits = ch - '?';
1,083,851✔
680
        for (i = 0; i < 6; ++i) {
7,437,352✔
681
            if ((bits & (1 << i)) != 0) {
6,365,153✔
682
                if (pos_x + repeat > width ||
1,536,784✔
683
                        row_offset + pos_y + i >= height) {
1,530,944!
684
                    fallback = 1;
11,676✔
685
                    status = (-1);
11,676✔
686
                    break;
11,676✔
687
                }
688
                chunk_cursor = sixel_local_buffer_reserve_row(
1,525,108✔
689
                    &local_buffer, pos_y + i);
477,200✔
690
                if (chunk_cursor == NULL) {
1,525,108!
691
                    fallback = 1;
×
692
                    status = (-1);
×
693
                    break;
×
694
                }
695

696
                row_base = (pos_y + i - chunk_cursor->start_row) * width +
2,002,308✔
697
                    pos_x;
477,200✔
698
                relative = (pos_y + i) * width + pos_x;
1,525,108✔
699

700
                if (pixel_size == 1 && repeat > 3) {
1,525,108✔
701
                    memset(chunk_cursor->data + (size_t)row_base,
4,592✔
702
                           color_index,
703
                           repeat);
704
                } else {
1,530✔
705
                    for (r = 0; r < repeat; ++r) {
3,064,755✔
706
                        sixel_decoder_parallel_store_pixel(
1,544,239✔
707
                            chunk_cursor->data +
2,026,434✔
708
                            (size_t)(row_base + r) * (size_t)pixel_size,
1,544,239✔
709
                            depth,
482,195✔
710
                            color_index,
482,195✔
711
                            context->palette);
482,195✔
712
                    }
482,195✔
713
                }
714
                written += repeat;
1,525,108✔
715
                if (min_relative < 0 || relative < min_relative) {
1,525,108!
716
                    min_relative = relative;
590✔
717
                }
566✔
718
                if (max_relative < relative + repeat - 1) {
1,525,132✔
719
                    max_relative = relative + repeat - 1;
66,913✔
720
                }
22,213✔
721
            }
477,224✔
722
        }
1,957,421✔
723

724
        if (fallback) {
1,072,211✔
725
            break;
12✔
726
        }
727

728
        cursor += 1;
1,072,199✔
729
        pos_x += repeat;
1,072,199✔
730
        repeat = 1;
1,072,199✔
731

732
    }
733

734
    copy_span = max_relative + 1;
72✔
735
    if (copy_span < 0) {
72!
736
        copy_span = 0;
×
737
    }
738

739
    if (logger != NULL) {
72!
NEW
740
        sixel_logger_logf(logger,
×
741
                          "decode",
742
                          "decoder",
743
                          fallback ? "abort" : "finish",
744
                          context->index,
745
                          context->index,
746
                          0,
747
                          0,
748
                          context->start_offset,
749
                          context->end_offset,
750
                          "worker %d decode wrote=%d status=%d",
751
                          context->index,
752
                          written,
753
                          status);
754
    }
755

756
    if (status != 0) {
72✔
757
        sixel_mutex_lock(&chain->mutex);
12✔
758
        chain->abort_requested = 1;
12✔
759
        sixel_cond_broadcast(&chain->cond);
12✔
760
        sixel_mutex_unlock(&chain->mutex);
12✔
761
        sixel_local_buffer_dispose(&local_buffer);
12✔
762
        context->result = status;
12✔
763
        return status;
12✔
764
    }
765

766
    context->local_buffer = local_buffer.head != NULL ?
100!
767
        local_buffer.head->data : NULL;
60!
768
    context->local_capacity = capacity;
60✔
769
    context->local_written = copy_span;
60✔
770

771
    sixel_mutex_lock(&chain->mutex);
60✔
772
    while (!chain->copy_ready[context->index] && !chain->abort_requested) {
64✔
773
        sixel_cond_wait(&chain->cond, &chain->mutex);
4✔
774
    }
775
    if (chain->abort_requested) {
60✔
776
        sixel_mutex_unlock(&chain->mutex);
36✔
777
        sixel_local_buffer_dispose(&local_buffer);
36✔
778
        context->local_buffer = NULL;
36✔
779
        context->local_capacity = 0;
36✔
780
        context->local_written = 0;
36✔
781
        status = (-1);
36✔
782
        context->result = status;
36✔
783
        return status;
36✔
784
    }
785
    chain_offset = chain->copy_offsets[context->index];
24✔
786
    copy_offset = chain_offset;
24✔
787
    next_offset = copy_offset + copy_span;
24✔
788
    context->local_written = copy_span;
24✔
789
    if (context->index + 1 < chain->thread_count) {
24✔
790
        chain->copy_offsets[context->index + 1] = next_offset;
18✔
791
        chain->copy_ready[context->index + 1] = 1;
18✔
792
    }
6✔
793
    sixel_cond_broadcast(&chain->cond);
24✔
794
    sixel_mutex_unlock(&chain->mutex);
24✔
795

796
    if (copy_offset < 0 || chain->global_buffer == NULL) {
24!
797
        sixel_mutex_lock(&chain->mutex);
×
798
        chain->abort_requested = 1;
×
799
        sixel_cond_broadcast(&chain->cond);
×
800
        sixel_mutex_unlock(&chain->mutex);
×
801
        sixel_local_buffer_dispose(&local_buffer);
×
802
        context->local_buffer = NULL;
×
803
        context->local_capacity = 0;
×
804
        context->local_written = 0;
×
805
        status = (-1);
×
806
        context->result = status;
×
807
        return status;
×
808
    }
809

810
    if (logger != NULL) {
24!
811
        /* Chain memcpy execution so the timeline shows the serialized copy */
NEW
812
        sixel_logger_logf(logger,
×
813
                          "copy",
814
                          "decoder",
815
                          "start",
816
                          context->index,
817
                          context->index,
818
                          0,
819
                          0,
820
                          copy_offset,
821
                          next_offset,
822
                          "worker %d memcpy count=%d",
823
                          context->index,
824
                          copy_span);
825
    }
826

827
    if (max_relative >= 0) {
24!
828
        chunk_cursor = local_buffer.head;
24✔
829
        while (chunk_cursor != NULL) {
54✔
830
            int rows;
831
            size_t chunk_bytes;
832
            size_t chunk_offset;
833

834
            rows = chunk_cursor->rows;
30✔
835
            if (chunk_cursor->start_row + rows >
40✔
836
                    (max_relative / width) + 1) {
30✔
837
                rows = (max_relative / width) + 1 -
32✔
838
                    chunk_cursor->start_row;
24✔
839
            }
8✔
840
            if (rows < 0) {
30!
841
                rows = 0;
×
842
            }
843
            if (rows > 0) {
30!
844
                chunk_bytes = (size_t)(rows * width) *
40✔
845
                    (size_t)chain->pixel_size;
30✔
846
                chunk_offset = (size_t)((row_offset +
60✔
847
                    chunk_cursor->start_row) * width) *
50✔
848
                    (size_t)chain->pixel_size;
30✔
849
                memcpy(chain->global_buffer + chunk_offset,
30✔
850
                       chunk_cursor->data,
20✔
851
                       chunk_bytes);
852
            }
10✔
853
            chunk_cursor = chunk_cursor->next;
30✔
854
        }
855
    }
8✔
856

857
    if (logger != NULL) {
24!
NEW
858
        sixel_logger_logf(logger,
×
859
                          "copy",
860
                          "decoder",
861
                          "finish",
862
                          context->index,
863
                          context->index,
864
                          0,
865
                          0,
866
                          copy_offset,
867
                          next_offset,
868
                          "worker %d memcpy done", 
869
                          context->index);
870
    }
871

872
    sixel_local_buffer_dispose(&local_buffer);
24✔
873
    context->local_buffer = NULL;
24✔
874
    context->local_capacity = 0;
24✔
875
    context->local_written = 0;
24✔
876

877
    context->result = status;
24✔
878
    return status;
24✔
879
}
24✔
880
#endif
881

882
static int
883
sixel_decoder_threads_token_is_auto(char const *text)
229✔
884
{
885
    if (text == NULL) {
229!
886
        return 0;
×
887
    }
888
    if ((text[0] == 'a' || text[0] == 'A') &&
229!
889
            (text[1] == 'u' || text[1] == 'U') &&
×
890
            (text[2] == 't' || text[2] == 'T') &&
×
891
            (text[3] == 'o' || text[3] == 'O') &&
×
892
            text[4] == '\0') {
×
893
        return 1;
×
894
    }
895
    return 0;
229✔
896
}
77✔
897

898
static int
899
sixel_decoder_threads_normalize(int requested)
449✔
900
{
901
    int normalized;
902

903
#if SIXEL_ENABLE_THREADS
904
    int hw_threads;
905

906
    if (requested <= 0) {
449!
907
        hw_threads = sixel_get_hw_threads();
×
908
        if (hw_threads < 1) {
×
909
            hw_threads = 1;
×
910
        }
911
        normalized = hw_threads;
×
912
    } else {
913
        normalized = requested;
449✔
914
    }
915
#else
916
    (void)requested;
917
    normalized = 1;
918
#endif
919
    if (normalized < 1) {
449!
920
        normalized = 1;
×
921
    }
922
    return normalized;
449✔
923
}
924

925
static int
926
sixel_decoder_threads_parse_value(char const *text, int *value)
229✔
927
{
928
    long parsed;
929
    char *endptr;
930
    int normalized;
931

932
    if (text == NULL || value == NULL) {
229!
933
        return 0;
×
934
    }
935
    if (sixel_decoder_threads_token_is_auto(text)) {
229!
936
        normalized = sixel_decoder_threads_normalize(0);
×
937
        *value = normalized;
×
938
        return 1;
×
939
    }
940
    errno = 0;
229✔
941
    parsed = strtol(text, &endptr, 10);
229✔
942
    if (endptr == text || *endptr != '\0' || errno == ERANGE) {
229!
943
        return 0;
3✔
944
    }
945
    if (parsed < 1) {
226!
946
        normalized = sixel_decoder_threads_normalize(1);
×
947
    } else if (parsed > INT_MAX) {
226!
948
        normalized = sixel_decoder_threads_normalize(INT_MAX);
×
949
    } else {
950
        normalized = sixel_decoder_threads_normalize((int)parsed);
226✔
951
    }
952
    *value = normalized;
226✔
953
    return 1;
226✔
954
}
77✔
955

956
static void
957
sixel_decoder_threads_load_env(void)
223✔
958
{
959
    char const *text;
960
    int parsed;
961

962
    if (g_decoder_threads.env_checked) {
223✔
963
        return;
3✔
964
    }
965
    g_decoder_threads.env_checked = 1;
220✔
966
    g_decoder_threads.env_valid = 0;
220✔
967
    text = getenv("SIXEL_THREADS");
220✔
968
    if (text == NULL || text[0] == '\0') {
220!
969
        return;
×
970
    }
971
    if (sixel_decoder_threads_parse_value(text, &parsed)) {
220!
972
        g_decoder_threads.env_threads = parsed;
220✔
973
        g_decoder_threads.env_valid = 1;
220✔
974
    }
74✔
975
}
75✔
976

977
SIXELSTATUS
978
sixel_decoder_parallel_override_threads(char const *text)
9✔
979
{
980
    SIXELSTATUS status;
981
    int parsed;
982

983
    status = SIXEL_BAD_ARGUMENT;
9✔
984
    if (text == NULL || text[0] == '\0') {
9!
985
        sixel_helper_set_additional_message(
×
986
            "decoder: missing thread count after -=/--threads.");
987
        goto end;
×
988
    }
989
    if (!sixel_decoder_threads_parse_value(text, &parsed)) {
9✔
990
        sixel_helper_set_additional_message(
3✔
991
            "decoder: threads must be a positive integer or 'auto'.");
992
        goto end;
3✔
993
    }
994
    g_decoder_threads.override_active = 1;
6✔
995
    g_decoder_threads.override_threads = parsed;
6✔
996
    status = SIXEL_OK;
6✔
997
end:
6✔
998
    return status;
9✔
999
}
1000

1001
int
1002
sixel_decoder_parallel_resolve_threads(void)
223✔
1003
{
1004
    int threads;
1005

1006
    threads = 1;
223✔
1007
    sixel_decoder_threads_load_env();
223✔
1008
    if (g_decoder_threads.override_active) {
223✔
1009
        threads = sixel_decoder_threads_normalize(
6✔
1010
            g_decoder_threads.override_threads);
2✔
1011
    } else if (g_decoder_threads.env_valid) {
219!
1012
        threads = sixel_decoder_threads_normalize(
217✔
1013
            g_decoder_threads.env_threads);
73✔
1014
    }
73✔
1015
    if (threads < 1) {
223!
1016
        threads = 1;
×
1017
    }
1018
    return threads;
223✔
1019
}
1020

1021
SIXELSTATUS
1022
sixel_decoder_parallel_request_start(int direct_mode,
223✔
1023
                                     unsigned char *input,
1024
                                     int length,
1025
                                     unsigned char *anchor,
1026
                                     image_buffer_t *image,
1027
                                     int const *palette,
1028
                                     sixel_logger_t *logger)
1029
{
1030
#if SIXEL_ENABLE_THREADS
1031
    SIXELSTATUS status;
1032
    int threads;
1033
    int payload_start;
1034
    int payload_len;
1035
    sixel_thread_t *workers;
1036
    sixel_decoder_worker_context_t *contexts;
1037
    sixel_decoder_worker_chain_t chain;
1038
    int *copy_offsets;
1039
    int *copy_ready;
1040
    int global_capacity;
1041
    int *spans;
1042
    int i;
1043
    int offset;
1044
    int created;
1045
    int parallel_failed;
1046
    int runtime_error;
1047
    int sync_ready;
1048
    int pixel_size;
1049
    int palette_limit;
1050

1051
    status = SIXEL_RUNTIME_ERROR;
223✔
1052
    workers = NULL;
223✔
1053
    contexts = NULL;
223✔
1054
    copy_offsets = NULL;
223✔
1055
    copy_ready = NULL;
223✔
1056
    spans = NULL;
223✔
1057
    threads = 0;
223✔
1058
    payload_start = 0;
223✔
1059
    payload_len = 0;
223✔
1060
    global_capacity = 0;
223✔
1061
    offset = 0;
223✔
1062
    created = 0;
223✔
1063
    parallel_failed = 0;
223✔
1064
    runtime_error = 0;
223✔
1065
    sync_ready = 0;
223✔
1066
    pixel_size = 4;
223✔
1067
    palette_limit = SIXEL_PALETTE_MAX_DECODER;
223✔
1068
    memset(&chain, 0, sizeof(chain));
223✔
1069

1070
    if (input == NULL || anchor == NULL || length <= 0 || image == NULL) {
223!
1071
        runtime_error = 1;
×
1072
        goto cleanup;
×
1073
    }
1074

1075
    pixel_size = sixel_decoder_parallel_pixel_size(image->depth);
223✔
1076

1077
    payload_start = (int)(anchor - input);
223✔
1078
    if (payload_start < 0 || payload_start >= length) {
223!
1079
        runtime_error = 1;
×
1080
        goto cleanup;
×
1081
    }
1082

1083
    payload_len = length - payload_start;
223✔
1084
    if (payload_len <= 0) {
223!
1085
        runtime_error = 1;
×
1086
        goto cleanup;
×
1087
    }
1088

1089
    palette_limit = image->ncolors;
223✔
1090
    if (palette_limit <= 0 || palette_limit > SIXEL_PALETTE_MAX_DECODER) {
223!
1091
        palette_limit = SIXEL_PALETTE_MAX_DECODER;
×
1092
    }
1093

1094
    threads = sixel_decoder_parallel_resolve_threads();
223✔
1095
    if (threads < 1) {
223!
1096
        threads = 1;
×
1097
    }
1098
    if (threads > payload_len) {
223!
1099
        threads = payload_len;
×
1100
    }
1101
    if (threads < 2) {
223✔
1102
        status = SIXEL_FALSE;
205✔
1103
        goto cleanup;
205✔
1104
    }
1105

1106
    workers = (sixel_thread_t *)calloc((size_t)threads,
18✔
1107
                                       sizeof(sixel_thread_t));
1108
    contexts = (sixel_decoder_worker_context_t *)calloc(
18✔
1109
        (size_t)threads, sizeof(sixel_decoder_worker_context_t));
6✔
1110
    spans = (int *)calloc((size_t)threads, sizeof(int));
18✔
1111
    copy_offsets = (int *)calloc((size_t)threads, sizeof(int));
18✔
1112
    copy_ready = (int *)calloc((size_t)threads, sizeof(int));
18✔
1113
    if (workers == NULL || contexts == NULL || spans == NULL ||
24!
1114
            copy_offsets == NULL || copy_ready == NULL) {
18!
1115
        runtime_error = 1;
×
1116
        goto cleanup;
×
1117
    }
1118

1119
    global_capacity = image->width * image->height;
18✔
1120
    if (global_capacity <= 0) {
18!
1121
        runtime_error = 1;
×
1122
        goto cleanup;
×
1123
    }
1124
    chain.global_buffer = (unsigned char *)image->pixels.p;
18✔
1125
    chain.pixel_size = pixel_size;
18✔
1126
    chain.copy_offsets = copy_offsets;
18✔
1127
    chain.copy_ready = copy_ready;
18✔
1128
    chain.thread_count = threads;
18✔
1129
    chain.global_capacity = global_capacity;
18✔
1130
    sixel_mutex_init(&chain.mutex);
18✔
1131
    sixel_cond_init(&chain.cond);
18✔
1132
    chain.copy_ready[0] = 1;
18✔
1133
    chain.copy_offsets[0] = 0;
18✔
1134
    sync_ready = 1;
18✔
1135

1136
    sixel_decoder_parallel_fill_spans(payload_len, threads, spans);
18✔
1137

1138
    if (logger != NULL) {
18!
1139
        /*
1140
         * Record when the controller hands control to worker threads so the
1141
         * timeline can show the gap between parser startup and worker
1142
         * creation.
1143
         */
NEW
1144
        sixel_logger_logf(logger,
×
1145
                          "decoder",
1146
                          "controller",
1147
                          "launch",
1148
                          0,
1149
                          0,
1150
                          0,
1151
                          length,
1152
                          payload_start,
1153
                          length,
1154
                          "spawn %d workers payload=%d",
1155
                          threads,
1156
                          payload_len);
1157
    }
1158

1159
    offset = payload_start;
18✔
1160
    created = 0;
18✔
1161
    for (i = 0; i < threads; ++i) {
90✔
1162
        contexts[i].chain = &chain;
72✔
1163
        contexts[i].input = input;
72✔
1164
        contexts[i].anchor = anchor;
72✔
1165
        contexts[i].length = length;
72✔
1166
        contexts[i].payload_len = payload_len;
72✔
1167
        contexts[i].start_offset = offset;
72✔
1168
        offset += spans[i];
72✔
1169
        if (offset > length) {
72!
1170
            offset = length;
×
1171
        }
1172
        if (i == threads - 1) {
72✔
1173
            contexts[i].end_offset = length - 1;
18✔
1174
        } else {
6✔
1175
            contexts[i].end_offset = offset - 1;
54✔
1176
            if (contexts[i].end_offset < contexts[i].start_offset) {
54!
1177
                contexts[i].end_offset = contexts[i].start_offset;
×
1178
            }
1179
        }
1180
        contexts[i].index = i;
72✔
1181
        contexts[i].direct_mode = direct_mode;
72✔
1182
        contexts[i].palette = palette;
72✔
1183
        contexts[i].palette_limit = palette_limit;
72✔
1184
        contexts[i].width = image->width;
72✔
1185
        contexts[i].height = image->height;
72✔
1186
        contexts[i].pixel_size = pixel_size;
72✔
1187
        contexts[i].depth = image->depth;
72✔
1188
        contexts[i].logger = logger;
72✔
1189
        contexts[i].result = (-1);
72✔
1190
        status = sixel_thread_create(&workers[i],
96✔
1191
                                     sixel_decoder_parallel_worker,
1192
                                     &contexts[i]);
72✔
1193
        if (SIXEL_FAILED(status)) {
72!
1194
            runtime_error = 1;
×
1195
            break;
×
1196
        }
1197
        created += 1;
72✔
1198
    }
24✔
1199

1200
    for (i = 0; i < created; ++i) {
90✔
1201
        sixel_thread_join(&workers[i]);
72✔
1202
        if (contexts[i].result != 0) {
72✔
1203
            parallel_failed = 1;
48✔
1204
        }
16✔
1205
    }
24✔
1206

1207
    if (chain.abort_requested) {
18✔
1208
        parallel_failed = 1;
12✔
1209
    }
4✔
1210

1211
    if (runtime_error || created < threads) {
24!
1212
        status = SIXEL_RUNTIME_ERROR;
×
1213
    } else if (parallel_failed) {
18✔
1214
        status = SIXEL_FALSE;
12✔
1215
    } else {
4✔
1216
        status = SIXEL_OK;
6✔
1217
    }
1218

1219
cleanup:
148✔
1220
    if (sync_ready) {
223✔
1221
        sixel_mutex_destroy(&chain.mutex);
18✔
1222
        sixel_cond_destroy(&chain.cond);
18✔
1223
    }
6✔
1224

1225
    free(workers);
223✔
1226
    free(contexts);
223✔
1227
    free(spans);
223✔
1228
    free(copy_offsets);
223✔
1229
    free(copy_ready);
223✔
1230

1231
    return status;
223✔
1232
#else
1233
    (void)direct_mode;
1234
    (void)input;
1235
    (void)length;
1236
    (void)anchor;
1237
    (void)palette;
1238
    (void)logger;
1239

1240
    return SIXEL_RUNTIME_ERROR;
1241
#endif
1242
}
1243

1244
SIXELSTATUS
1245
sixel_decode_raw_parallel(unsigned char *p,
208✔
1246
                          int len,
1247
                          image_buffer_t *image,
1248
                          sixel_allocator_t *allocator,
1249
                          int *used_parallel)
1250
{
1251
    SIXELSTATUS status;
1252
#if SIXEL_ENABLE_THREADS
1253
    sixel_logger_t logger;
1254
#endif
1255

1256
    (void)p;
70✔
1257
    (void)len;
70✔
1258
    (void)image;
70✔
1259
    (void)allocator;
70✔
1260

1261
    status = SIXEL_OK;
208✔
1262
#if SIXEL_ENABLE_THREADS
1263
    sixel_logger_init(&logger);
208✔
1264
    (void)sixel_logger_prepare_env(&logger);
208✔
1265
    sixel_decoder_parallel_log_stub(&logger, "raw");
208✔
1266
#endif
1267
    if (used_parallel != NULL) {
208!
1268
        *used_parallel = 0;
208✔
1269
    }
70✔
1270
#if SIXEL_ENABLE_THREADS
1271
    sixel_logger_close(&logger);
208✔
1272
#endif
1273
    return status;
208✔
1274
}
1275

1276
SIXELSTATUS
1277
sixel_decode_direct_parallel(unsigned char *p,
18✔
1278
                             int len,
1279
                             image_buffer_t *image,
1280
                             sixel_allocator_t *allocator,
1281
                             int *used_parallel)
1282
{
1283
    SIXELSTATUS status;
1284
#if SIXEL_ENABLE_THREADS
1285
    sixel_logger_t logger;
1286
#endif
1287

1288
    (void)p;
6✔
1289
    (void)len;
6✔
1290
    (void)image;
6✔
1291
    (void)allocator;
6✔
1292

1293
    status = SIXEL_OK;
18✔
1294
#if SIXEL_ENABLE_THREADS
1295
    sixel_logger_init(&logger);
18✔
1296
    (void)sixel_logger_prepare_env(&logger);
18✔
1297
    sixel_decoder_parallel_log_stub(&logger, "direct");
18✔
1298
#endif
1299
    if (used_parallel != NULL) {
18!
1300
        *used_parallel = 0;
18✔
1301
    }
6✔
1302
#if SIXEL_ENABLE_THREADS
1303
    sixel_logger_close(&logger);
18✔
1304
#endif
1305
    return status;
18✔
1306
}
1307

1308
/* emacs Local Variables:      */
1309
/* emacs mode: c               */
1310
/* emacs tab-width: 4          */
1311
/* emacs indent-tabs-mode: nil */
1312
/* emacs c-basic-offset: 4     */
1313
/* emacs End:                  */
1314
/* vim: set expandtab ts=4 sts=4 sw=4 : */
1315
/* EOF */
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc