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

saitoha / libsixel / 19934796867

04 Dec 2025 03:42PM UTC coverage: 43.522% (+2.3%) from 41.258%
19934796867

push

github

saitoha
python: update shared api.py

10714 of 38654 branches covered (27.72%)

14673 of 33714 relevant lines covered (43.52%)

2910517.57 hits per line

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

75.9
/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_fill_spans(int payload_len,
100
                                  int threads,
101
                                  int *spans);
102

103
static int
104
sixel_decoder_parallel_skew_percent(void)
4✔
105
{
106
    char const *text;
4✔
107
    char *endptr;
4✔
108
    long value;
4✔
109

110
    /*
111
     * SIXEL_PARALLEL_SKEW lets operators bias span lengths by +/-20% so the
112
     * trailing workers take slightly more work.  The default keeps spans
113
     * balanced.
114
     */
115
    text = getenv("SIXEL_PARALLEL_SKEW");
4✔
116
    if (text == NULL || text[0] == '\0') {
4!
117
        return 0;
118
    }
119

120
    errno = 0;
×
121
    value = strtol(text, &endptr, 10);
×
122
    if (errno != 0 || endptr == text || *endptr != '\0') {
×
123
        return 0;
124
    }
125

126
    if (value < -20) {
×
127
        value = -20;
128
    } else if (value > 20) {
×
129
        value = 20;
130
    }
131

132
    return (int)value;
×
133
}
134

135
static void
136
sixel_decoder_parallel_fill_spans(int payload_len,
4✔
137
                                  int threads,
138
                                  int *spans)
139
{
140
    int base_share;
4✔
141
    int skew_percent;
4✔
142
    double skew;
4✔
143
    int i;
4✔
144
    double center;
4✔
145
    int total;
4✔
146
    int remainder;
4✔
147

148
    base_share = payload_len / threads;
4✔
149
    skew_percent = sixel_decoder_parallel_skew_percent();
4✔
150
    skew = ((double)base_share * (double)skew_percent) / 100.0;
4✔
151
    total = 0;
4✔
152
    for (i = 0; i < threads; ++i) {
20✔
153
        center = (double)i - (double)(threads - 1) / 2.0;
16✔
154
        spans[i] = base_share + (int)(skew * center);
16✔
155
        if (spans[i] < 1) {
16!
156
            spans[i] = 1;
×
157
        }
158
        total += spans[i];
16✔
159
    }
160

161
    remainder = payload_len - total;
4✔
162
    while (remainder > 0) {
4!
163
        for (i = threads - 1; i >= 0 && remainder > 0; --i) {
×
164
            spans[i] += 1;
×
165
            --remainder;
×
166
        }
167
    }
168
    while (remainder < 0) {
4!
169
        for (i = threads - 1; i >= 0 && remainder < 0; --i) {
×
170
            if (spans[i] > 1) {
×
171
                spans[i] -= 1;
×
172
                ++remainder;
×
173
            }
174
        }
175
        if (remainder < 0) {
×
176
            break;
177
        }
178
    }
179
}
4✔
180

181
static int
182
sixel_decoder_parallel_pixel_size(int depth)
136✔
183
{
184
    int pixel_size;
136✔
185

186
    pixel_size = 4;
136✔
187
    if (depth == 1) {
136✔
188
        pixel_size = 1;
189
    } else if (depth == 2) {
8!
190
        pixel_size = 2;
×
191
    }
192

193
    return pixel_size;
136✔
194
}
195

196
static void
197
sixel_decoder_parallel_store_pixel(unsigned char *dst,
576✔
198
                                   int depth,
199
                                   int color_index,
200
                                   int const *palette)
201
{
202
    int color;
576✔
203

204
    if (depth == 1) {
576!
205
        dst[0] = (unsigned char)color_index;
×
206
        return;
×
207
    }
208

209
    if (depth == 2) {
576!
210
        unsigned short packed;
×
211

212
        packed = (unsigned short)color_index;
×
213
        memcpy(dst, &packed, sizeof(packed));
×
214
        return;
×
215
    }
216

217
    color = 0;
576✔
218
    if (palette != NULL &&
576!
219
            color_index >= 0 &&
576!
220
            color_index < SIXEL_PALETTE_MAX_DECODER) {
221
        color = palette[color_index];
576✔
222
    }
223
    dst[0] = (unsigned char)((color >> 16) & 0xff);
576✔
224
    dst[1] = (unsigned char)((color >> 8) & 0xff);
576✔
225
    dst[2] = (unsigned char)(color & 0xff);
576✔
226
    dst[3] = 255u;
576✔
227
}
1!
228

229
static void
230
sixel_local_buffer_init(sixel_local_buffer_t *buffer,
16✔
231
                        int width,
232
                        int pixel_size)
233
{
234
    buffer->head = NULL;
16✔
235
    buffer->tail = NULL;
16✔
236
    buffer->cursor = NULL;
16✔
237
    buffer->width = width;
16✔
238
    buffer->pixel_size = pixel_size;
16✔
239
}
240

241
static void
242
sixel_local_buffer_dispose(sixel_local_buffer_t *buffer)
16✔
243
{
244
    sixel_decoder_local_chunk_t *cursor;
16✔
245

246
    cursor = buffer->head;
16✔
247
    while (cursor != NULL) {
32✔
248
        sixel_decoder_local_chunk_t *tmp;
16✔
249

250
        free(cursor->data);
16✔
251
        tmp = cursor->next;
16✔
252
        free(cursor);
16✔
253
        cursor = tmp;
16✔
254
    }
255

256
    buffer->head = NULL;
16✔
257
    buffer->tail = NULL;
16✔
258
    buffer->cursor = NULL;
16✔
259
}
16✔
260

261
static sixel_decoder_local_chunk_t *
262
sixel_local_buffer_append(sixel_local_buffer_t *buffer,
16✔
263
                          int rows,
264
                          int start_row)
265
{
266
    sixel_decoder_local_chunk_t *chunk;
16✔
267
    size_t bytes;
16✔
268

269
    if (rows % 6 != 0) {
16!
270
        rows += 6 - (rows % 6);
×
271
    }
272

273
    chunk = (sixel_decoder_local_chunk_t *)calloc(
16✔
274
        1, sizeof(sixel_decoder_local_chunk_t));
275
    if (chunk == NULL) {
16!
276
        return NULL;
277
    }
278

279
    bytes = (size_t)(rows * buffer->width) * (size_t)buffer->pixel_size;
16✔
280
    chunk->data = (unsigned char *)calloc(bytes, 1);
16✔
281
    if (chunk->data == NULL) {
16!
282
        free(chunk);
×
283
        return NULL;
×
284
    }
285

286
    chunk->start_row = start_row;
16✔
287
    chunk->rows = rows;
16✔
288
    chunk->next = NULL;
16✔
289

290
    if (buffer->head == NULL) {
16!
291
        buffer->head = chunk;
16✔
292
    } else {
293
        buffer->tail->next = chunk;
×
294
    }
295
    buffer->tail = chunk;
16✔
296
    buffer->cursor = chunk;
16✔
297

298
    return chunk;
16✔
299
}
300

301
static sixel_decoder_local_chunk_t *
302
sixel_local_buffer_reserve_row(sixel_local_buffer_t *buffer,
192✔
303
                               int row)
304
{
305
    sixel_decoder_local_chunk_t *cursor;
192✔
306

307
    cursor = buffer->cursor;
192✔
308
    while (cursor != NULL && row >= cursor->start_row + cursor->rows) {
192!
309
        cursor = cursor->next;
×
310
    }
311
    if (cursor != NULL && row >= cursor->start_row) {
192!
312
        buffer->cursor = cursor;
192✔
313
        return cursor;
192✔
314
    }
315

316
    cursor = buffer->tail;
×
317
    if (cursor == NULL) {
×
318
        return NULL;
319
    }
320

321
    while (row >= cursor->start_row + cursor->rows) {
×
322
        sixel_decoder_local_chunk_t *next;
×
323
        int start_row;
×
324

325
        start_row = cursor->start_row + cursor->rows;
×
326
        next = sixel_local_buffer_append(buffer, cursor->rows, start_row);
×
327
        if (next == NULL) {
×
328
            return NULL;
329
        }
330
        cursor = next;
331
    }
×
332

333
    buffer->cursor = cursor;
×
334
    return cursor;
×
335
}
336

337
/*
338
 * Worker entry for the fast sixel parser.  Each thread jumps to the next
339
 * '-' marker from its assigned offset, then walks tokens until the first
340
 * '-' after its scheduled end offset.
341
 *
342
 * Supported tokens: palette switches ('#n'), carriage return ('$'), next
343
 * line ('-'), and raster data ('?' - '~').  Any raster attribute reset '"',
344
 * palette redefinition '#n;type;...', or CAN/SUB control code marks the
345
 * chunk as invalid and returns a failure status so the caller can fall back.
346
 */
347
static int
348
sixel_decoder_parallel_worker(void *arg)
16✔
349
{
350
    sixel_decoder_worker_context_t *context;
16✔
351
    sixel_decoder_worker_chain_t *chain;
16✔
352
    sixel_local_buffer_t local_buffer;
16✔
353
    sixel_decoder_local_chunk_t *chunk_cursor = NULL;
16✔
354
    unsigned char *anchor = NULL;
16✔
355
    unsigned char *scan = NULL;
16✔
356
    unsigned char *cursor = NULL;
16✔
357
    unsigned char *stop = NULL;
16✔
358
    unsigned char *limit = NULL;
16✔
359
    unsigned char *start = NULL;
16✔
360
    int capacity = 0;
16✔
361
    int written = 0;
16✔
362
    int assigned = 0;
16✔
363
    int bits = 0;
16✔
364
    int repeat = 1;
16✔
365
    int color_index = 0;
16✔
366
    int max_relative = (-1);
16✔
367
    int min_relative = (-1);
16✔
368
    int pos_x = 0;
16✔
369
    int pos_y = 0;
16✔
370
    int relative = 0;
16✔
371
    int row_base = 0;
16✔
372
    int r = 0;
16✔
373
    int row_offset = 0;
16✔
374
    int line_count = 0;
16✔
375
    int copy_offset = 0;
16✔
376
    int copy_span = 0;
16✔
377
    int next_offset = 0;
16✔
378
    int i;
16✔
379
    int fallback = 0;
16✔
380
    int status = (-1);
16✔
381
    sixel_logger_t *logger = NULL;
16✔
382
    int width = 0;
16✔
383
    int pixel_size = 0;
16✔
384
    int depth = 0;
16✔
385
    int height = 0;
16✔
386
    int chain_offset = 0;
16✔
387
    unsigned char ch;
16✔
388

389
    context = (sixel_decoder_worker_context_t *)arg;
16✔
390
    if (context == NULL) {
16!
391
        return (-1);
392
    }
393

394
    context->result = status;
16✔
395

396
    chain = context->chain;
16✔
397
    if (chain == NULL) {
16!
398
        context->result = status;
399
        return status;
400
    }
401

402
    logger = context->logger;
16✔
403
    anchor = context->anchor;
16✔
404
    if (context->input == NULL || context->length <= 0) {
16!
405
        context->result = status;
406
        return status;
407
    }
408

409
    width = context->width;
16✔
410
    if (width <= 0) {
16!
411
        context->result = status;
412
        return status;
413
    }
414

415
    pixel_size = context->pixel_size;
16✔
416
    depth = context->depth;
16✔
417
    height = context->height;
16✔
418
    if (height <= 0) {
16!
419
        context->result = status;
420
        return status;
421
    }
422
    sixel_local_buffer_init(&local_buffer, width, pixel_size);
16✔
423
    if (context->payload_len <= 0) {
16!
424
        context->result = status;
425
        return status;
426
    }
427

428
    start = context->input + context->start_offset;
16✔
429
    if (start < context->input ||
16!
430
            start >= context->input + context->length) {
16!
431
        context->result = status;
432
        return status;
433
    }
434

435
    assigned = context->end_offset - context->start_offset + 1;
16✔
436
    if (assigned <= 0) {
16!
437
        context->result = status;
438
        return status;
439
    }
440

441
    status = 0;
16✔
442

443
    if (logger != NULL) {
16!
444
        /*
445
         * Decode window for this worker. The key keeps decode spans grouped
446
         * per-thread in the timeline output.
447
         */
448
        sixel_logger_logf(logger,
×
449
                          "decode",
450
                          "decoder",
451
                          "start",
452
                          context->index,
453
                          context->index,
454
                          0,
455
                          0,
456
                          context->start_offset,
457
                          context->end_offset,
458
                          "worker %d decode span [%d,%d]",
459
                          context->index,
460
                          context->start_offset,
461
                          context->end_offset);
462
    }
463

464
    cursor = start;
16✔
465
    if (context->index > 0) {
16✔
466
        cursor = (unsigned char *)memchr(start,
12✔
467
                                         '-',
468
                                         (size_t)(context->length -
12✔
469
                                         context->start_offset));
12✔
470
        if (cursor != NULL &&
12!
471
                cursor + 1 < context->input + context->length) {
12!
472
            cursor += 1;
16✔
473
        } else {
474
            cursor = start;
475
        }
476
    }
477
    if (context->index > 0 && cursor == start) {
16!
478
        status = (-1);
×
479
        context->result = status;
×
480
        return status;
×
481
    }
482

483
    if (anchor != NULL && anchor < cursor) {
16!
484
        scan = anchor;
485
        while (scan < cursor) {
3,876✔
486
            if (*scan == '-') {
3,864✔
487
                line_count += 1;
12✔
488
                scan += 1;
12✔
489
                continue;
12✔
490
            }
491

492
            if (*scan == '#') {
3,852✔
493
                int value;
768✔
494
                unsigned char *p;
768✔
495

496
                value = 0;
768✔
497
                p = scan + 1;
768✔
498
                while (p < cursor && *p >= '0' && *p <= '9') {
2,184!
499
                    value = value * 10 + (*p - '0');
1,416✔
500
                    p += 1;
1,416✔
501
                }
502
                color_index = value;
768✔
503
                if (color_index < 0) {
768!
504
                    color_index = 0;
505
                }
506
                if (color_index >= SIXEL_PALETTE_MAX_DECODER) {
768!
507
                    color_index = SIXEL_PALETTE_MAX_DECODER - 1;
508
                }
509
                if (p < cursor && *p == ';') {
768!
510
                    scan = p;
511
                    continue;
512
                }
513
                scan = p;
768✔
514
                continue;
768✔
515
            }
516

517
            scan += 1;
3,084✔
518
        }
519
    }
520
    row_offset = line_count * 6;
16✔
521

522
    capacity = (int)((double)chain->global_capacity *
16✔
523
        ((double)assigned / (double)context->payload_len));
16✔
524
    capacity = (int)((double)capacity * 1.10);
16✔
525
    if (capacity < 1) {
16!
526
        capacity = 1;
527
    }
528
    if (capacity < width * 6) {
16!
529
        capacity = width * 6;
530
    }
531
    if (capacity % (width * 6) != 0) {
16!
532
        capacity += (width * 6) - (capacity % (width * 6));
×
533
    }
534

535
    chunk_cursor = sixel_local_buffer_append(&local_buffer,
16✔
536
                                             capacity / width,
537
                                             0);
538
    if (chunk_cursor == NULL) {
16!
539
        status = (-1);
×
540
        context->result = status;
×
541
        return status;
×
542
    }
543
    capacity = chunk_cursor->rows * width;
16✔
544

545
    stop = context->input + context->end_offset;
16✔
546
    limit = context->input + context->length;
16✔
547
    while (cursor < limit) {
168!
548
        ch = *cursor;
168✔
549

550
        if (ch < 0x20) {
168!
551
            if (ch == 0x18 || ch == 0x1a) {
60!
552
                fallback = 1;
553
                status = (-1);
554
                break;
555
            }
556
            cursor += 1;
60✔
557
            if (ch == 0x1b && cursor < limit && *cursor == '\\') {
60!
558
                status = (0);
559
                break;
560
            }
561
            continue;
48✔
562
        }
563

564
        if (ch == '"') {
1!
565
            fallback = 1;
566
            status = (-1);
567
            break;
568
        }
569

570
        if (ch == '!') {
36✔
571
            int value;
36✔
572
            unsigned char *p;
36✔
573

574
            value = 0;
36✔
575
            p = cursor + 1;
36✔
576
            while (p < limit && *p >= '0' && *p <= '9') {
72!
577
                value = value * 10 + (*p - '0');
36✔
578
                p += 1;
36✔
579
            }
580
            if (value <= 0) {
36!
581
                value = 1;
582
            }
583
            repeat = value;
36✔
584
            cursor = p;
36✔
585
            continue;
36✔
586
        }
587

588
        if (ch == '#') {
36✔
589
            int value;
36✔
590
            unsigned char *p;
36✔
591

592
            value = 0;
36✔
593
            p = cursor + 1;
36✔
594
            while (p < limit && *p >= '0' && *p <= '9') {
72!
595
                value = value * 10 + (*p - '0');
36✔
596
                p += 1;
36✔
597
            }
598
            if (p < limit && *p == ';') {
36!
599
                fallback = 1;
600
                status = (-1);
601
                break;
602
            }
603
            color_index = value;
36✔
604
            if (color_index < 0) {
36!
605
                color_index = 0;
606
            }
607
            if (color_index >= SIXEL_PALETTE_MAX_DECODER) {
36!
608
                color_index = SIXEL_PALETTE_MAX_DECODER - 1;
609
            }
610
            cursor = p;
36✔
611
            continue;
36✔
612
        }
1!
613

614
        if (ch == '$') {
×
615
            cursor += 1;
×
616
            pos_x = 0;
×
617
            continue;
×
618
        }
619

620
        if (ch == '-') {
×
621
            if (cursor >= stop) {
×
622
                break;
623
            }
624
            cursor += 1;
×
625
            pos_x = 0;
×
626
            pos_y += 6;
×
627
            chunk_cursor = sixel_local_buffer_reserve_row(&local_buffer,
×
628
                                                          pos_y);
629
            if (chunk_cursor == NULL) {
×
630
                fallback = 1;
631
                status = (-1);
632
                break;
633
            }
634
            continue;
×
635
        }
636

637
        if (ch < '?' || ch > '~') {
×
638
            cursor += 1;
×
639
            continue;
×
640
        }
641

642
        bits = ch - '?';
36✔
643
        for (i = 0; i < 6; ++i) {
228✔
644
            if ((bits & (1 << i)) != 0) {
196!
645
                if (pos_x + repeat > width ||
196!
646
                        row_offset + pos_y + i >= height) {
192!
647
                    fallback = 1;
648
                    status = (-1);
649
                    break;
650
                }
651
                chunk_cursor = sixel_local_buffer_reserve_row(
192✔
652
                    &local_buffer, pos_y + i);
653
                if (chunk_cursor == NULL) {
192!
654
                    fallback = 1;
655
                    status = (-1);
656
                    break;
657
                }
658

659
                row_base = (pos_y + i - chunk_cursor->start_row) * width +
192✔
660
                    pos_x;
661
                relative = (pos_y + i) * width + pos_x;
192✔
662

663
                if (pixel_size == 1 && repeat > 3) {
192!
664
                    memset(chunk_cursor->data + (size_t)row_base,
96✔
665
                           color_index,
666
                           repeat);
667
                } else {
668
                    for (r = 0; r < repeat; ++r) {
672✔
669
                        sixel_decoder_parallel_store_pixel(
576✔
670
                            chunk_cursor->data +
576✔
671
                            (size_t)(row_base + r) * (size_t)pixel_size,
576✔
672
                            depth,
673
                            color_index,
674
                            context->palette);
675
                    }
676
                }
677
                written += repeat;
192✔
678
                if (min_relative < 0 || relative < min_relative) {
192!
679
                    min_relative = relative;
680
                }
681
                if (max_relative < relative + repeat - 1) {
192✔
682
                    max_relative = relative + repeat - 1;
683
                }
684
            }
685
        }
686

687
        if (fallback) {
36✔
688
            break;
689
        }
690

691
        cursor += 1;
32✔
692
        pos_x += repeat;
32✔
693
        repeat = 1;
32✔
694

695
    }
696

697
    copy_span = max_relative + 1;
16✔
698
    if (copy_span < 0) {
16!
699
        copy_span = 0;
700
    }
701

702
    if (logger != NULL) {
16!
703
        sixel_logger_logf(logger,
×
704
                          "decode",
705
                          "decoder",
706
                          fallback ? "abort" : "finish",
707
                          context->index,
708
                          context->index,
709
                          0,
710
                          0,
711
                          context->start_offset,
712
                          context->end_offset,
713
                          "worker %d decode wrote=%d status=%d",
714
                          context->index,
715
                          written,
716
                          status);
717
    }
718

719
    if (status != 0) {
16✔
720
        sixel_mutex_lock(&chain->mutex);
4✔
721
        chain->abort_requested = 1;
4✔
722
        sixel_cond_broadcast(&chain->cond);
4✔
723
        sixel_mutex_unlock(&chain->mutex);
4✔
724
        sixel_local_buffer_dispose(&local_buffer);
4✔
725
        context->result = status;
4✔
726
        return status;
4✔
727
    }
728

729
    context->local_buffer = local_buffer.head != NULL ?
24!
730
        local_buffer.head->data : NULL;
12!
731
    context->local_capacity = capacity;
12✔
732
    context->local_written = copy_span;
12✔
733

734
    sixel_mutex_lock(&chain->mutex);
12✔
735
    while (!chain->copy_ready[context->index] && !chain->abort_requested) {
12!
736
        sixel_cond_wait(&chain->cond, &chain->mutex);
×
737
    }
738
    if (chain->abort_requested) {
12!
739
        sixel_mutex_unlock(&chain->mutex);
12✔
740
        sixel_local_buffer_dispose(&local_buffer);
12✔
741
        context->local_buffer = NULL;
12✔
742
        context->local_capacity = 0;
12✔
743
        context->local_written = 0;
12✔
744
        status = (-1);
12✔
745
        context->result = status;
12✔
746
        return status;
12✔
747
    }
748
    chain_offset = chain->copy_offsets[context->index];
×
749
    copy_offset = chain_offset;
×
750
    next_offset = copy_offset + copy_span;
×
751
    context->local_written = copy_span;
×
752
    if (context->index + 1 < chain->thread_count) {
×
753
        chain->copy_offsets[context->index + 1] = next_offset;
×
754
        chain->copy_ready[context->index + 1] = 1;
×
755
    }
756
    sixel_cond_broadcast(&chain->cond);
×
757
    sixel_mutex_unlock(&chain->mutex);
×
758

759
    if (copy_offset < 0 || chain->global_buffer == NULL) {
×
760
        sixel_mutex_lock(&chain->mutex);
×
761
        chain->abort_requested = 1;
×
762
        sixel_cond_broadcast(&chain->cond);
×
763
        sixel_mutex_unlock(&chain->mutex);
×
764
        sixel_local_buffer_dispose(&local_buffer);
×
765
        context->local_buffer = NULL;
×
766
        context->local_capacity = 0;
×
767
        context->local_written = 0;
×
768
        status = (-1);
×
769
        context->result = status;
×
770
        return status;
×
771
    }
772

773
    if (logger != NULL) {
×
774
        /* Chain memcpy execution so the timeline shows the serialized copy */
775
        sixel_logger_logf(logger,
×
776
                          "copy",
777
                          "decoder",
778
                          "start",
779
                          context->index,
780
                          context->index,
781
                          0,
782
                          0,
783
                          copy_offset,
784
                          next_offset,
785
                          "worker %d memcpy count=%d",
786
                          context->index,
787
                          copy_span);
788
    }
789

790
    if (max_relative >= 0) {
×
791
        chunk_cursor = local_buffer.head;
×
792
        while (chunk_cursor != NULL) {
×
793
            int rows;
×
794
            size_t chunk_bytes;
×
795
            size_t chunk_offset;
×
796

797
            rows = chunk_cursor->rows;
×
798
            if (chunk_cursor->start_row + rows >
×
799
                    (max_relative / width) + 1) {
×
800
                rows = (max_relative / width) + 1 -
×
801
                    chunk_cursor->start_row;
802
            }
803
            if (rows < 0) {
×
804
                rows = 0;
805
            }
806
            if (rows > 0) {
×
807
                chunk_bytes = (size_t)(rows * width) *
×
808
                    (size_t)chain->pixel_size;
×
809
                chunk_offset = (size_t)((row_offset +
×
810
                    chunk_cursor->start_row) * width) *
×
811
                    (size_t)chain->pixel_size;
812
                memcpy(chain->global_buffer + chunk_offset,
×
813
                       chunk_cursor->data,
×
814
                       chunk_bytes);
815
            }
816
            chunk_cursor = chunk_cursor->next;
×
817
        }
818
    }
819

820
    if (logger != NULL) {
×
821
        sixel_logger_logf(logger,
×
822
                          "copy",
823
                          "decoder",
824
                          "finish",
825
                          context->index,
826
                          context->index,
827
                          0,
828
                          0,
829
                          copy_offset,
830
                          next_offset,
831
                          "worker %d memcpy done", 
832
                          context->index);
833
    }
834

835
    sixel_local_buffer_dispose(&local_buffer);
×
836
    context->local_buffer = NULL;
×
837
    context->local_capacity = 0;
×
838
    context->local_written = 0;
×
839

840
    context->result = status;
×
841
    return status;
×
842
}
843
#endif
844

845
static int
846
sixel_decoder_threads_token_is_auto(char const *text)
136✔
847
{
848
    if (text == NULL) {
136!
849
        return 0;
850
    }
851
    if ((text[0] == 'a' || text[0] == 'A') &&
136!
852
            (text[1] == 'u' || text[1] == 'U') &&
×
853
            (text[2] == 't' || text[2] == 'T') &&
×
854
            (text[3] == 'o' || text[3] == 'O') &&
×
855
            text[4] == '\0') {
×
856
        return 1;
×
857
    }
858
    return 0;
859
}
860

861
static int
862
sixel_decoder_threads_normalize(int requested)
136✔
863
{
864
    int normalized;
136✔
865

866
#if SIXEL_ENABLE_THREADS
867
    int hw_threads;
136✔
868

869
    if (requested <= 0) {
136!
870
        hw_threads = sixel_get_hw_threads();
×
871
        if (hw_threads < 1) {
×
872
            hw_threads = 1;
873
        }
874
        normalized = hw_threads;
875
    } else {
876
        normalized = requested;
877
    }
878
#else
879
    (void)requested;
880
    normalized = 1;
881
#endif
882
    if (normalized < 1) {
1!
883
        normalized = 1;
884
    }
885
    return normalized;
×
886
}
887

888
static int
889
sixel_decoder_threads_parse_value(char const *text, int *value)
136✔
890
{
891
    long parsed;
136✔
892
    char *endptr;
136✔
893
    int normalized;
136✔
894

895
    if (text == NULL || value == NULL) {
136!
896
        return 0;
897
    }
898
    if (sixel_decoder_threads_token_is_auto(text)) {
136!
899
        normalized = sixel_decoder_threads_normalize(0);
×
900
        *value = normalized;
×
901
        return 1;
×
902
    }
903
    errno = 0;
136✔
904
    parsed = strtol(text, &endptr, 10);
136✔
905
    if (endptr == text || *endptr != '\0' || errno == ERANGE) {
136!
906
        return 0;
907
    }
908
    if (parsed < 1) {
134!
909
        normalized = sixel_decoder_threads_normalize(1);
910
    } else if (parsed > INT_MAX) {
134!
911
        normalized = sixel_decoder_threads_normalize(INT_MAX);
912
    } else {
913
        normalized = sixel_decoder_threads_normalize((int)parsed);
134✔
914
    }
915
    *value = normalized;
134✔
916
    return 1;
134✔
917
}
918

919
static void
920
sixel_decoder_threads_load_env(void)
136✔
921
{
922
    char const *text;
136✔
923
    int parsed;
136✔
924

925
    if (g_decoder_threads.env_checked) {
136✔
926
        return;
2✔
927
    }
928
    g_decoder_threads.env_checked = 1;
134✔
929
    g_decoder_threads.env_valid = 0;
134✔
930
    text = getenv("SIXEL_THREADS");
134✔
931
    if (text == NULL || text[0] == '\0') {
134!
932
        return;
933
    }
934
    if (sixel_decoder_threads_parse_value(text, &parsed)) {
134!
935
        g_decoder_threads.env_threads = parsed;
134✔
936
        g_decoder_threads.env_valid = 1;
134✔
937
    }
938
}
1!
939

940
SIXELSTATUS
941
sixel_decoder_parallel_override_threads(char const *text)
2✔
942
{
943
    SIXELSTATUS status;
2✔
944
    int parsed;
2✔
945

946
    status = SIXEL_BAD_ARGUMENT;
2✔
947
    if (text == NULL || text[0] == '\0') {
2!
948
        sixel_helper_set_additional_message(
×
949
            "decoder: missing thread count after -=/--threads.");
950
        goto end;
×
951
    }
952
    if (!sixel_decoder_threads_parse_value(text, &parsed)) {
2!
953
        sixel_helper_set_additional_message(
2✔
954
            "decoder: threads must be a positive integer or 'auto'.");
955
        goto end;
2✔
956
    }
957
    g_decoder_threads.override_active = 1;
×
958
    g_decoder_threads.override_threads = parsed;
×
959
    status = SIXEL_OK;
×
960
end:
2✔
961
    return status;
2✔
962
}
963

964
int
965
sixel_decoder_parallel_resolve_threads(void)
136✔
966
{
967
    int threads;
136✔
968

969
    threads = 1;
136✔
970
    sixel_decoder_threads_load_env();
136✔
971
    if (g_decoder_threads.override_active) {
136!
972
        threads = sixel_decoder_threads_normalize(
×
973
            g_decoder_threads.override_threads);
974
    } else if (g_decoder_threads.env_valid) {
136!
975
        threads = sixel_decoder_threads_normalize(
136!
976
            g_decoder_threads.env_threads);
977
    }
978
    if (threads < 1) {
×
979
        threads = 1;
980
    }
981
    return threads;
136✔
982
}
983

984
SIXELSTATUS
985
sixel_decoder_parallel_request_start(int direct_mode,
136✔
986
                                     unsigned char *input,
987
                                     int length,
988
                                     unsigned char *anchor,
989
                                     image_buffer_t *image,
990
                                     int const *palette,
991
                                     sixel_logger_t *logger)
992
{
993
#if SIXEL_ENABLE_THREADS
994
    SIXELSTATUS status;
136✔
995
    int threads;
136✔
996
    int payload_start;
136✔
997
    int payload_len;
136✔
998
    sixel_thread_t *workers;
136✔
999
    sixel_decoder_worker_context_t *contexts;
136✔
1000
    sixel_decoder_worker_chain_t chain;
136✔
1001
    int *copy_offsets;
136✔
1002
    int *copy_ready;
136✔
1003
    int global_capacity;
136✔
1004
    int *spans;
136✔
1005
    int i;
136✔
1006
    int offset;
136✔
1007
    int created;
136✔
1008
    int parallel_failed;
136✔
1009
    int runtime_error;
136✔
1010
    int sync_ready;
136✔
1011
    int pixel_size;
136✔
1012
    int palette_limit;
136✔
1013

1014
    status = SIXEL_RUNTIME_ERROR;
136✔
1015
    workers = NULL;
136✔
1016
    contexts = NULL;
136✔
1017
    copy_offsets = NULL;
136✔
1018
    copy_ready = NULL;
136✔
1019
    spans = NULL;
136✔
1020
    threads = 0;
136✔
1021
    payload_start = 0;
136✔
1022
    payload_len = 0;
136✔
1023
    global_capacity = 0;
136✔
1024
    offset = 0;
136✔
1025
    created = 0;
136✔
1026
    parallel_failed = 0;
136✔
1027
    runtime_error = 0;
136✔
1028
    sync_ready = 0;
136✔
1029
    pixel_size = 4;
136✔
1030
    palette_limit = SIXEL_PALETTE_MAX_DECODER;
136✔
1031
    memset(&chain, 0, sizeof(chain));
136!
1032

1033
    if (input == NULL || anchor == NULL || length <= 0 || image == NULL) {
136!
1034
        runtime_error = 1;
×
1035
        goto cleanup;
×
1036
    }
1037

1038
    pixel_size = sixel_decoder_parallel_pixel_size(image->depth);
136✔
1039

1040
    payload_start = (int)(anchor - input);
136✔
1041
    if (payload_start < 0 || payload_start >= length) {
136!
1042
        runtime_error = 1;
×
1043
        goto cleanup;
×
1044
    }
1045

1046
    payload_len = length - payload_start;
136✔
1047
    if (payload_len <= 0) {
136!
1048
        runtime_error = 1;
1049
        goto cleanup;
1050
    }
1051

1052
    palette_limit = image->ncolors;
136✔
1053
    if (palette_limit <= 0 || palette_limit > SIXEL_PALETTE_MAX_DECODER) {
136!
1054
        palette_limit = SIXEL_PALETTE_MAX_DECODER;
×
1055
    }
1056

1057
    threads = sixel_decoder_parallel_resolve_threads();
136✔
1058
    if (threads < 1) {
136!
1059
        threads = 1;
1060
    }
1061
    if (threads > payload_len) {
136!
1062
        threads = payload_len;
1063
    }
1064
    if (threads < 2) {
136✔
1065
        status = SIXEL_FALSE;
132✔
1066
        goto cleanup;
132✔
1067
    }
1068

1069
    workers = (sixel_thread_t *)calloc((size_t)threads,
4✔
1070
                                       sizeof(sixel_thread_t));
1071
    contexts = (sixel_decoder_worker_context_t *)calloc(
4✔
1072
        (size_t)threads, sizeof(sixel_decoder_worker_context_t));
1073
    spans = (int *)calloc((size_t)threads, sizeof(int));
4✔
1074
    copy_offsets = (int *)calloc((size_t)threads, sizeof(int));
4✔
1075
    copy_ready = (int *)calloc((size_t)threads, sizeof(int));
4✔
1076
    if (workers == NULL || contexts == NULL || spans == NULL ||
4!
1077
            copy_offsets == NULL || copy_ready == NULL) {
4!
1078
        runtime_error = 1;
×
1079
        goto cleanup;
×
1080
    }
1081

1082
    global_capacity = image->width * image->height;
4✔
1083
    if (global_capacity <= 0) {
4!
1084
        runtime_error = 1;
×
1085
        goto cleanup;
×
1086
    }
1087
    chain.global_buffer = (unsigned char *)image->pixels.p;
4✔
1088
    chain.pixel_size = pixel_size;
4✔
1089
    chain.copy_offsets = copy_offsets;
4✔
1090
    chain.copy_ready = copy_ready;
4✔
1091
    chain.thread_count = threads;
4✔
1092
    chain.global_capacity = global_capacity;
4✔
1093
    sixel_mutex_init(&chain.mutex);
4✔
1094
    sixel_cond_init(&chain.cond);
4✔
1095
    chain.copy_ready[0] = 1;
4✔
1096
    chain.copy_offsets[0] = 0;
4✔
1097
    sync_ready = 1;
4✔
1098

1099
    sixel_decoder_parallel_fill_spans(payload_len, threads, spans);
4✔
1100

1101
    if (logger != NULL) {
4!
1102
        /*
1103
         * Record when the controller hands control to worker threads so the
1104
         * timeline can show the gap between parser startup and worker
1105
         * creation.
1106
         */
1107
        sixel_logger_logf(logger,
×
1108
                          "decoder",
1109
                          "controller",
1110
                          "launch",
1111
                          0,
1112
                          0,
1113
                          0,
1114
                          length,
1115
                          payload_start,
1116
                          length,
1117
                          "spawn %d workers payload=%d",
1118
                          threads,
1119
                          payload_len);
1120
    }
1121

1122
    offset = payload_start;
1123
    created = 0;
1124
    for (i = 0; i < threads; ++i) {
20✔
1125
        contexts[i].chain = &chain;
16✔
1126
        contexts[i].input = input;
16✔
1127
        contexts[i].anchor = anchor;
16✔
1128
        contexts[i].length = length;
16✔
1129
        contexts[i].payload_len = payload_len;
16✔
1130
        contexts[i].start_offset = offset;
16✔
1131
        offset += spans[i];
16✔
1132
        if (offset > length) {
16!
1133
            offset = length;
1134
        }
1135
        if (i == threads - 1) {
16✔
1136
            contexts[i].end_offset = length - 1;
4✔
1137
        } else {
1138
            contexts[i].end_offset = offset - 1;
12✔
1139
            if (contexts[i].end_offset < contexts[i].start_offset) {
12!
1140
                contexts[i].end_offset = contexts[i].start_offset;
×
1141
            }
1142
        }
1143
        contexts[i].index = i;
16✔
1144
        contexts[i].direct_mode = direct_mode;
16✔
1145
        contexts[i].palette = palette;
16✔
1146
        contexts[i].palette_limit = palette_limit;
16✔
1147
        contexts[i].width = image->width;
16✔
1148
        contexts[i].height = image->height;
16✔
1149
        contexts[i].pixel_size = pixel_size;
16✔
1150
        contexts[i].depth = image->depth;
16✔
1151
        contexts[i].logger = logger;
16✔
1152
        contexts[i].result = (-1);
16✔
1153
        status = sixel_thread_create(&workers[i],
16✔
1154
                                     sixel_decoder_parallel_worker,
1155
                                     &contexts[i]);
1156
        if (SIXEL_FAILED(status)) {
16!
1157
            runtime_error = 1;
1158
            break;
1159
        }
1160
        created += 1;
16✔
1161
    }
1162

1163
    for (i = 0; i < created; ++i) {
20✔
1164
        sixel_thread_join(&workers[i]);
16✔
1165
        if (contexts[i].result != 0) {
16!
1166
            parallel_failed = 1;
16✔
1167
        }
1168
    }
1169

1170
    if (chain.abort_requested) {
4!
1171
        parallel_failed = 1;
4✔
1172
    }
1173

1174
    if (runtime_error || created < threads) {
4!
1175
        status = SIXEL_RUNTIME_ERROR;
1176
    } else if (parallel_failed) {
4!
1177
        status = SIXEL_FALSE;
1178
    } else {
1179
        status = SIXEL_OK;
1180
    }
1181

1182
cleanup:
1183
    if (sync_ready) {
132✔
1184
        sixel_mutex_destroy(&chain.mutex);
4✔
1185
        sixel_cond_destroy(&chain.cond);
4✔
1186
    }
1187

1188
    free(workers);
136✔
1189
    free(contexts);
136✔
1190
    free(spans);
136✔
1191
    free(copy_offsets);
136✔
1192
    free(copy_ready);
136✔
1193

1194
    return status;
136✔
1195
#else
1196
    (void)direct_mode;
1197
    (void)input;
1198
    (void)length;
1199
    (void)anchor;
1200
    (void)image;
1201
    (void)palette;
1202
    (void)logger;
1203

1204
    return SIXEL_RUNTIME_ERROR;
1205
#endif
1206
}
1207

1208
/* emacs Local Variables:      */
1209
/* emacs mode: c               */
1210
/* emacs tab-width: 4          */
1211
/* emacs indent-tabs-mode: nil */
1212
/* emacs c-basic-offset: 4     */
1213
/* emacs End:                  */
1214
/* vim: set expandtab ts=4 sts=4 sw=4 : */
1215
/* 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