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

saitoha / libsixel / 20815644050

08 Jan 2026 11:42AM UTC coverage: 55.168% (-1.5%) from 56.702%
20815644050

push

github

saitoha
ci: refine

21172 of 38377 relevant lines covered (55.17%)

1148322.52 hits per line

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

76.55
/src/decoder-parallel.c
1
/*
2
 * SPDX-License-Identifier: MIT
3
 *
4
 * Copyright (c) 2025 libsixel developers. See `AUTHORS`.
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
7
 * this software and associated documentation files (the "Software"), to deal in
8
 * the Software without restriction, including without limitation the rights to
9
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
 * the Software, and to permit persons to whom the Software is furnished to do so,
11
 * subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in all
14
 * copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 */
23

24
#if defined(HAVE_CONFIG_H)
25
#include "config.h"
26
#endif
27

28
#include <stdlib.h>
29
#include <string.h>
30
#if HAVE_ERRNO_H
31
# include <errno.h>
32
#endif  /* HAVE_ERRNO_H */
33
#if HAVE_LIMITS_H
34
# include <limits.h>
35
#endif /* HAVE_LIMITS_H */
36

37
#include <sixel.h>
38

39
#include "decoder-parallel.h"
40
#if SIXEL_ENABLE_THREADS
41
# include "logger.h"
42
# include "threading.h"
43
#endif
44
#include "compat_stub.h"
45

46
/*
47
 * The previous prescan-based parallel decoder has been removed.
48
 * The current implementation keeps the public entry points so that
49
 * callers can fall back to the single threaded decoder while we
50
 * prepare the new chunked worker pipeline.
51
 */
52

53
typedef struct sixel_decoder_thread_config {
54
    int env_checked;
55
    int env_valid;
56
    int env_threads;
57
    int override_active;
58
    int override_threads;
59
} sixel_decoder_thread_config_t;
60

61
#if SIXEL_ENABLE_THREADS
62
typedef struct sixel_decoder_worker_context {
63
    struct sixel_decoder_worker_chain *chain;
64
    unsigned char *input;
65
    unsigned char *anchor;
66
    int length;
67
    int payload_len;
68
    int start_offset;
69
    int end_offset;
70
    int index;
71
    int direct_mode;
72
    int const *palette;
73
    int palette_limit;
74
    int width;
75
    int height;
76
    int pixel_size;
77
    int depth;
78
    sixel_logger_t *logger;
79
    unsigned char *local_buffer;
80
    int local_capacity;
81
    int local_written;
82
    int result;
83
} sixel_decoder_worker_context_t;
84

85
typedef struct sixel_decoder_local_chunk {
86
    unsigned char *data;
87
    int start_row;
88
    int rows;
89
    struct sixel_decoder_local_chunk *next;
90
} sixel_decoder_local_chunk_t;
91

92
typedef struct sixel_local_buffer {
93
    sixel_decoder_local_chunk_t *head;
94
    sixel_decoder_local_chunk_t *tail;
95
    sixel_decoder_local_chunk_t *cursor;
96
    int width;
97
    int pixel_size;
98
} sixel_local_buffer_t;
99

100
typedef struct sixel_decoder_worker_chain {
101
    sixel_mutex_t mutex;
102
    sixel_cond_t cond;
103
    int *copy_offsets;
104
    int *copy_ready;
105
    int thread_count;
106
    unsigned char *global_buffer;
107
    int global_capacity;
108
    int pixel_size;
109
    int abort_requested;
110
} sixel_decoder_worker_chain_t;
111
#endif
112

113
static sixel_decoder_thread_config_t g_decoder_threads = {
114
    0,
115
    0,
116
    1,
117
    0,
118
    1
119
};
120

121
#if SIXEL_ENABLE_THREADS
122
static void
123
sixel_decoder_parallel_fill_spans(int payload_len,
124
                                  int threads,
125
                                  int *spans);
126

127
static int
128
sixel_decoder_parallel_skew_percent(void)
3✔
129
{
130
    char const *text;
3✔
131
    char *endptr;
3✔
132
    long value;
3✔
133

134
    /*
135
     * SIXEL_PARALLEL_SKEW lets operators bias span lengths by +/-20% so the
136
     * trailing workers take slightly more work.  The default keeps spans
137
     * balanced.
138
     */
139
    text = sixel_compat_getenv("SIXEL_PARALLEL_SKEW");
3✔
140
    if (text == NULL || text[0] == '\0') {
3✔
141
        return 0;
142
    }
143

144
    errno = 0;
×
145
    value = strtol(text, &endptr, 10);
×
146
    if (errno != 0 || endptr == text || *endptr != '\0') {
×
147
        return 0;
148
    }
149

150
    if (value < -20) {
×
151
        value = -20;
152
    } else if (value > 20) {
×
153
        value = 20;
154
    }
155

156
    return (int)value;
157
}
158

159
static void
160
sixel_decoder_parallel_fill_spans(int payload_len,
3✔
161
                                  int threads,
162
                                  int *spans)
163
{
164
    int base_share;
3✔
165
    int skew_percent;
3✔
166
    double skew;
3✔
167
    int i;
3✔
168
    double center;
3✔
169
    int total;
3✔
170
    int remainder;
3✔
171

172
    base_share = payload_len / threads;
3✔
173
    skew_percent = sixel_decoder_parallel_skew_percent();
3✔
174
    skew = ((double)base_share * (double)skew_percent) / 100.0;
3✔
175
    total = 0;
3✔
176
    for (i = 0; i < threads; ++i) {
15✔
177
        center = (double)i - (double)(threads - 1) / 2.0;
12✔
178
        spans[i] = base_share + (int)(skew * center);
12✔
179
        if (spans[i] < 1) {
12✔
180
            spans[i] = 1;
×
181
        }
182
        total += spans[i];
12✔
183
    }
184

185
    remainder = payload_len - total;
3✔
186
    while (remainder > 0) {
3✔
187
        for (i = threads - 1; i >= 0 && remainder > 0; --i) {
×
188
            spans[i] += 1;
×
189
            --remainder;
×
190
        }
191
    }
192
    while (remainder < 0) {
3✔
193
        for (i = threads - 1; i >= 0 && remainder < 0; --i) {
×
194
            if (spans[i] > 1) {
×
195
                spans[i] -= 1;
×
196
                ++remainder;
×
197
            }
198
        }
199
        if (remainder < 0) {
200
            break;
201
        }
202
    }
203
}
3✔
204

205
static int
206
sixel_decoder_parallel_pixel_size(int depth)
75✔
207
{
208
    int pixel_size;
75✔
209

210
    pixel_size = 4;
75✔
211
    if (depth == 1) {
75✔
212
        pixel_size = 1;
213
    } else if (depth == 2) {
5✔
214
        pixel_size = 2;
×
215
    }
216

217
    return pixel_size;
75✔
218
}
219

220
static void
221
sixel_decoder_parallel_store_pixel(unsigned char *dst,
576✔
222
                                   int depth,
223
                                   int color_index,
224
                                   int const *palette)
225
{
226
    int color;
576✔
227

228
    if (depth == 1) {
576✔
229
        dst[0] = (unsigned char)color_index;
×
230
        return;
×
231
    }
232

233
    if (depth == 2) {
576✔
234
        unsigned short packed;
×
235

236
        packed = (unsigned short)color_index;
×
237
        memcpy(dst, &packed, sizeof(packed));
×
238
        return;
×
239
    }
240

241
    color = 0;
576✔
242
    if (palette != NULL &&
576✔
243
            color_index >= 0 &&
576✔
244
            color_index < SIXEL_PALETTE_MAX_DECODER) {
245
        color = palette[color_index];
576✔
246
    }
247
    dst[0] = (unsigned char)((color >> 16) & 0xff);
576✔
248
    dst[1] = (unsigned char)((color >> 8) & 0xff);
576✔
249
    dst[2] = (unsigned char)(color & 0xff);
576✔
250
    dst[3] = 255u;
576✔
251
}
252

253
static void
254
sixel_local_buffer_init(sixel_local_buffer_t *buffer,
12✔
255
                        int width,
256
                        int pixel_size)
257
{
258
    buffer->head = NULL;
12✔
259
    buffer->tail = NULL;
12✔
260
    buffer->cursor = NULL;
12✔
261
    buffer->width = width;
12✔
262
    buffer->pixel_size = pixel_size;
12✔
263
}
264

265
static void
266
sixel_local_buffer_dispose(sixel_local_buffer_t *buffer)
12✔
267
{
268
    sixel_decoder_local_chunk_t *cursor;
12✔
269

270
    cursor = buffer->head;
12✔
271
    while (cursor != NULL) {
24✔
272
        sixel_decoder_local_chunk_t *tmp;
12✔
273

274
        free(cursor->data);
12✔
275
        tmp = cursor->next;
12✔
276
        free(cursor);
12✔
277
        cursor = tmp;
12✔
278
    }
279

280
    buffer->head = NULL;
12✔
281
    buffer->tail = NULL;
12✔
282
    buffer->cursor = NULL;
12✔
283
}
12✔
284

285
static sixel_decoder_local_chunk_t *
286
sixel_local_buffer_append(sixel_local_buffer_t *buffer,
12✔
287
                          int rows,
288
                          int start_row)
289
{
290
    sixel_decoder_local_chunk_t *chunk;
12✔
291
    size_t bytes;
12✔
292

293
    if (rows % 6 != 0) {
12✔
294
        rows += 6 - (rows % 6);
×
295
    }
296

297
    chunk = (sixel_decoder_local_chunk_t *)calloc(
12✔
298
        1, sizeof(sixel_decoder_local_chunk_t));
299
    if (chunk == NULL) {
12✔
300
        return NULL;
301
    }
302

303
    bytes = (size_t)(rows * buffer->width) * (size_t)buffer->pixel_size;
12✔
304
    chunk->data = (unsigned char *)calloc(bytes, 1);
12✔
305
    if (chunk->data == NULL) {
12✔
306
        free(chunk);
×
307
        return NULL;
×
308
    }
309

310
    chunk->start_row = start_row;
12✔
311
    chunk->rows = rows;
12✔
312
    chunk->next = NULL;
12✔
313

314
    if (buffer->head == NULL) {
12✔
315
        buffer->head = chunk;
12✔
316
    } else {
317
        buffer->tail->next = chunk;
×
318
    }
319
    buffer->tail = chunk;
12✔
320
    buffer->cursor = chunk;
12✔
321

322
    return chunk;
12✔
323
}
324

325
static sixel_decoder_local_chunk_t *
326
sixel_local_buffer_reserve_row(sixel_local_buffer_t *buffer,
144✔
327
                               int row)
328
{
329
    sixel_decoder_local_chunk_t *cursor;
144✔
330

331
    cursor = buffer->cursor;
144✔
332
    while (cursor != NULL && row >= cursor->start_row + cursor->rows) {
144✔
333
        cursor = cursor->next;
×
334
    }
335
    if (cursor != NULL && row >= cursor->start_row) {
144✔
336
        buffer->cursor = cursor;
144✔
337
        return cursor;
144✔
338
    }
339

340
    cursor = buffer->tail;
×
341
    if (cursor == NULL) {
×
342
        return NULL;
343
    }
344

345
    while (row >= cursor->start_row + cursor->rows) {
×
346
        sixel_decoder_local_chunk_t *next;
×
347
        int start_row;
×
348

349
        start_row = cursor->start_row + cursor->rows;
×
350
        next = sixel_local_buffer_append(buffer, cursor->rows, start_row);
×
351
        if (next == NULL) {
×
352
            return NULL;
353
        }
354
        cursor = next;
355
    }
356

357
    buffer->cursor = cursor;
×
358
    return cursor;
×
359
}
360

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

413
    context = (sixel_decoder_worker_context_t *)arg;
12✔
414
    if (context == NULL) {
12✔
415
        return (-1);
416
    }
417

418
    context->result = status;
12✔
419

420
    chain = context->chain;
12✔
421
    if (chain == NULL) {
12✔
422
        context->result = status;
423
        return status;
424
    }
425

426
    logger = context->logger;
12✔
427
    anchor = context->anchor;
12✔
428
    if (context->input == NULL || context->length <= 0) {
12✔
429
        context->result = status;
430
        return status;
431
    }
432

433
    width = context->width;
12✔
434
    if (width <= 0) {
12✔
435
        context->result = status;
436
        return status;
437
    }
438

439
    pixel_size = context->pixel_size;
12✔
440
    depth = context->depth;
12✔
441
    height = context->height;
12✔
442
    if (height <= 0) {
12✔
443
        context->result = status;
444
        return status;
445
    }
446
    sixel_local_buffer_init(&local_buffer, width, pixel_size);
12✔
447
    if (context->payload_len <= 0) {
12✔
448
        context->result = status;
449
        return status;
450
    }
451

452
    start = context->input + context->start_offset;
12✔
453
    if (start < context->input ||
12✔
454
            start >= context->input + context->length) {
12✔
455
        context->result = status;
456
        return status;
457
    }
458

459
    assigned = context->end_offset - context->start_offset + 1;
12✔
460
    if (assigned <= 0) {
12✔
461
        context->result = status;
462
        return status;
463
    }
464

465
    status = 0;
12✔
466

467
    if (logger != NULL) {
12✔
468
        /*
469
         * Decode window for this worker. The key keeps decode spans grouped
470
         * per-thread in the timeline output.
471
         */
472
        sixel_logger_logf(logger,
×
473
                          "decode",
474
                          "decoder",
475
                          "start",
476
                          context->index,
477
                          context->index,
478
                          0,
479
                          0,
480
                          context->start_offset,
481
                          context->end_offset,
482
                          "worker %d decode span [%d,%d]",
483
                          context->index,
484
                          context->start_offset,
485
                          context->end_offset);
486
    }
487

488
    cursor = start;
12✔
489
    if (context->index > 0) {
12✔
490
        cursor = (unsigned char *)memchr(start,
9✔
491
                                         '-',
492
                                         (size_t)(context->length -
9✔
493
                                         context->start_offset));
9✔
494
        if (cursor != NULL &&
9✔
495
                cursor + 1 < context->input + context->length) {
9✔
496
            cursor += 1;
12✔
497
        } else {
498
            cursor = start;
499
        }
500
    }
501
    if (context->index > 0 && cursor == start) {
12✔
502
        status = (-1);
×
503
        context->result = status;
×
504
        return status;
×
505
    }
506

507
    if (anchor != NULL && anchor < cursor) {
12✔
508
        scan = anchor;
509
        while (scan < cursor) {
2,907✔
510
            if (*scan == '-') {
2,898✔
511
                line_count += 1;
9✔
512
                scan += 1;
9✔
513
                continue;
9✔
514
            }
515

516
            if (*scan == '#') {
2,889✔
517
                int value;
576✔
518
                unsigned char *p;
576✔
519

520
                value = 0;
576✔
521
                p = scan + 1;
576✔
522
                while (p < cursor && *p >= '0' && *p <= '9') {
1,638✔
523
                    value = value * 10 + (*p - '0');
1,062✔
524
                    p += 1;
1,062✔
525
                }
526
                color_index = value;
576✔
527
                if (color_index < 0) {
576✔
528
                    color_index = 0;
529
                }
530
                if (color_index >= SIXEL_PALETTE_MAX_DECODER) {
576✔
531
                    color_index = SIXEL_PALETTE_MAX_DECODER - 1;
532
                }
533
                if (p < cursor && *p == ';') {
576✔
534
                    scan = p;
535
                    continue;
536
                }
537
                scan = p;
576✔
538
                continue;
576✔
539
            }
540

541
            scan += 1;
2,313✔
542
        }
543
    }
544
    row_offset = line_count * 6;
12✔
545

546
    capacity = (int)((double)chain->global_capacity *
12✔
547
        ((double)assigned / (double)context->payload_len));
12✔
548
    capacity = (int)((double)capacity * 1.10);
12✔
549
    if (capacity < 1) {
12✔
550
        capacity = 1;
551
    }
552
    if (capacity < width * 6) {
12✔
553
        capacity = width * 6;
554
    }
555
    if (capacity % (width * 6) != 0) {
12✔
556
        capacity += (width * 6) - (capacity % (width * 6));
×
557
    }
558

559
    chunk_cursor = sixel_local_buffer_append(&local_buffer,
12✔
560
                                             capacity / width,
561
                                             0);
562
    if (chunk_cursor == NULL) {
12✔
563
        status = (-1);
×
564
        context->result = status;
×
565
        return status;
×
566
    }
567
    capacity = chunk_cursor->rows * width;
12✔
568

569
    stop = context->input + context->end_offset;
12✔
570
    limit = context->input + context->length;
12✔
571
    while (cursor < limit) {
126✔
572
        /*
573
         * Hot path prefers raster tokens ('?' - '~') to reduce branching.
574
         * Control and attribute tokens fall back to the slow path below.
575
         */
576
        ch = *cursor;
126✔
577

578
        /*
579
         * Branch ordering follows observed frequency:
580
         *   raster ('?' - '~') > '#' > '!' > '$' > '-'
581
         *   >>> control (< 0x20) > '"'.
582
         */
583
        if (ch >= '?' && ch <= '~') {
126✔
584
            bits = ch - '?';
27✔
585
            for (i = 0; i < 6; ++i) {
171✔
586
                if ((bits & (1 << i)) != 0) {
147✔
587
                    if (pos_x + repeat > width ||
147✔
588
                            row_offset + pos_y + i >= height) {
144✔
589
                        fallback = 1;
590
                        status = (-1);
591
                        break;
592
                    }
593
                    chunk_cursor = sixel_local_buffer_reserve_row(
144✔
594
                        &local_buffer, pos_y + i);
595
                    if (chunk_cursor == NULL) {
144✔
596
                        fallback = 1;
597
                        status = (-1);
598
                        break;
599
                    }
600

601
                    row_base = (pos_y + i - chunk_cursor->start_row) *
144✔
602
                        width + pos_x;
603
                    relative = (pos_y + i) * width + pos_x;
144✔
604

605
                    if (pixel_size == 1 && repeat > 3) {
144✔
606
                        memset(chunk_cursor->data + (size_t)row_base,
48✔
607
                               color_index,
608
                               repeat);
609
                    } else {
610
                        for (r = 0; r < repeat; ++r) {
672✔
611
                            sixel_decoder_parallel_store_pixel(
576✔
612
                                chunk_cursor->data +
576✔
613
                                (size_t)(row_base + r) *
576✔
614
                                (size_t)pixel_size,
576✔
615
                                depth,
616
                                color_index,
617
                                context->palette);
618
                        }
619
                    }
620
                    written += repeat;
144✔
621
                    if (min_relative < 0 || relative < min_relative) {
144✔
622
                        min_relative = relative;
623
                    }
624
                    if (max_relative < relative + repeat - 1) {
144✔
625
                        max_relative = relative + repeat - 1;
626
                    }
627
                }
628
            }
629

630
            if (fallback) {
27✔
631
                break;
632
            }
633

634
            cursor += 1;
24✔
635
            pos_x += repeat;
24✔
636
            repeat = 1;
24✔
637
            continue;
24✔
638
        }
639

640
        if (ch == '#') {
27✔
641
            int value;
27✔
642
            unsigned char *p;
27✔
643

644
            value = 0;
27✔
645
            p = cursor + 1;
27✔
646
            while (p < limit && *p >= '0' && *p <= '9') {
54✔
647
                value = value * 10 + (*p - '0');
27✔
648
                p += 1;
27✔
649
            }
650
            if (p < limit && *p == ';') {
27✔
651
                fallback = 1;
652
                status = (-1);
653
                break;
654
            }
655
            color_index = value;
27✔
656
            if (color_index < 0) {
27✔
657
                color_index = 0;
658
            }
659
            if (color_index >= SIXEL_PALETTE_MAX_DECODER) {
27✔
660
                color_index = SIXEL_PALETTE_MAX_DECODER - 1;
661
            }
662
            cursor = p;
27✔
663
            continue;
27✔
664
        }
665

666
        if (ch == '!') {
27✔
667
            int value;
27✔
668
            unsigned char *p;
27✔
669

670
            value = 0;
27✔
671
            p = cursor + 1;
27✔
672
            while (p < limit && *p >= '0' && *p <= '9') {
54✔
673
                value = value * 10 + (*p - '0');
27✔
674
                p += 1;
27✔
675
            }
676
            if (value <= 0) {
27✔
677
                value = 1;
678
            }
679
            repeat = value;
27✔
680
            cursor = p;
27✔
681
            continue;
27✔
682
        }
683

684
        if (ch == '$') {
×
685
            cursor += 1;
×
686
            pos_x = 0;
×
687
            continue;
×
688
        }
689

690
        if (ch == '-') {
×
691
            if (cursor >= stop) {
×
692
                break;
693
            }
694
            cursor += 1;
×
695
            pos_x = 0;
×
696
            pos_y += 6;
×
697
            chunk_cursor = sixel_local_buffer_reserve_row(&local_buffer,
×
698
                                                          pos_y);
699
            if (chunk_cursor == NULL) {
×
700
                fallback = 1;
701
                status = (-1);
702
                break;
703
            }
704
            continue;
×
705
        }
706

707
        if (ch < 0x20) {
36✔
708
            if (ch == 0x18 || ch == 0x1a) {
45✔
709
                fallback = 1;
710
                status = (-1);
711
                break;
712
            }
713
            cursor += 1;
45✔
714
            if (ch == 0x1b && cursor < limit && *cursor == '\\') {
45✔
715
                status = (0);
716
                break;
717
            }
718
            continue;
36✔
719
        }
720

721
        if (ch == '"') {
722
            fallback = 1;
723
            status = (-1);
724
            break;
725
        }
726

727
        cursor += 1;
×
728
    }
729

730
    copy_span = max_relative + 1;
12✔
731
    if (copy_span < 0) {
12✔
732
        copy_span = 0;
733
    }
734

735
    if (logger != NULL) {
12✔
736
        sixel_logger_logf(logger,
×
737
                          "decode",
738
                          "decoder",
739
                          fallback ? "abort" : "finish",
740
                          context->index,
741
                          context->index,
742
                          0,
743
                          0,
744
                          context->start_offset,
745
                          context->end_offset,
746
                          "worker %d decode wrote=%d status=%d",
747
                          context->index,
748
                          written,
749
                          status);
750
    }
751

752
    if (status != 0) {
12✔
753
        sixel_mutex_lock(&chain->mutex);
3✔
754
        chain->abort_requested = 1;
3✔
755
        sixel_cond_broadcast(&chain->cond);
3✔
756
        sixel_mutex_unlock(&chain->mutex);
3✔
757
        sixel_local_buffer_dispose(&local_buffer);
3✔
758
        context->result = status;
3✔
759
        return status;
3✔
760
    }
761

762
    context->local_buffer = local_buffer.head != NULL ?
18✔
763
        local_buffer.head->data : NULL;
9✔
764
    context->local_capacity = capacity;
9✔
765
    context->local_written = copy_span;
9✔
766

767
    sixel_mutex_lock(&chain->mutex);
9✔
768
    while (!chain->copy_ready[context->index] && !chain->abort_requested) {
12✔
769
        sixel_cond_wait(&chain->cond, &chain->mutex);
3✔
770
    }
771
    if (chain->abort_requested) {
9✔
772
        sixel_mutex_unlock(&chain->mutex);
9✔
773
        sixel_local_buffer_dispose(&local_buffer);
9✔
774
        context->local_buffer = NULL;
9✔
775
        context->local_capacity = 0;
9✔
776
        context->local_written = 0;
9✔
777
        status = (-1);
9✔
778
        context->result = status;
9✔
779
        return status;
9✔
780
    }
781
    chain_offset = chain->copy_offsets[context->index];
×
782
    copy_offset = chain_offset;
×
783
    next_offset = copy_offset + copy_span;
×
784
    context->local_written = copy_span;
×
785
    if (context->index + 1 < chain->thread_count) {
×
786
        chain->copy_offsets[context->index + 1] = next_offset;
×
787
        chain->copy_ready[context->index + 1] = 1;
×
788
    }
789
    sixel_cond_broadcast(&chain->cond);
×
790
    sixel_mutex_unlock(&chain->mutex);
×
791

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

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

823
    if (max_relative >= 0) {
×
824
        chunk_cursor = local_buffer.head;
×
825
        while (chunk_cursor != NULL) {
×
826
            int rows;
×
827
            size_t chunk_bytes;
×
828
            size_t chunk_offset;
×
829

830
            rows = chunk_cursor->rows;
×
831
            if (chunk_cursor->start_row + rows >
×
832
                    (max_relative / width) + 1) {
×
833
                rows = (max_relative / width) + 1 -
×
834
                    chunk_cursor->start_row;
835
            }
836
            if (rows < 0) {
×
837
                rows = 0;
838
            }
839
            if (rows > 0) {
×
840
                chunk_bytes = (size_t)(rows * width) *
×
841
                    (size_t)chain->pixel_size;
×
842
                chunk_offset = (size_t)((row_offset +
×
843
                    chunk_cursor->start_row) * width) *
×
844
                    (size_t)chain->pixel_size;
845
                memcpy(chain->global_buffer + chunk_offset,
×
846
                       chunk_cursor->data,
×
847
                       chunk_bytes);
848
            }
849
            chunk_cursor = chunk_cursor->next;
×
850
        }
851
    }
852

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

868
    sixel_local_buffer_dispose(&local_buffer);
×
869
    context->local_buffer = NULL;
×
870
    context->local_capacity = 0;
×
871
    context->local_written = 0;
×
872

873
    context->result = status;
×
874
    return status;
×
875
}
876
#endif
877

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

894
static int
895
sixel_decoder_threads_normalize(int requested)
72✔
896
{
897
    int normalized;
72✔
898

899
#if SIXEL_ENABLE_THREADS
900
    int hw_threads;
72✔
901

902
    if (requested <= 0) {
72✔
903
        hw_threads = sixel_get_hw_threads();
×
904
        if (hw_threads < 1) {
×
905
            hw_threads = 1;
906
        }
907
        normalized = hw_threads;
908
    } else {
909
        normalized = requested;
910
    }
911
#else
912
    (void)requested;
913
    normalized = 1;
914
#endif
915
    if (normalized < 1) {
916
        normalized = 1;
917
    }
918
    return normalized;
×
919
}
920

921
static int
922
sixel_decoder_threads_parse_value(char const *text, int *value)
72✔
923
{
924
    long parsed;
72✔
925
    char *endptr;
72✔
926
    int normalized;
72✔
927

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

952
static void
953
sixel_decoder_threads_load_env(void)
75✔
954
{
955
    char const *text;
75✔
956
    int parsed;
75✔
957

958
    if (g_decoder_threads.env_checked) {
75✔
959
        return;
4✔
960
    }
961
    g_decoder_threads.env_checked = 1;
74✔
962
    g_decoder_threads.env_valid = 0;
74✔
963
    text = sixel_compat_getenv("SIXEL_THREADS");
74✔
964
    if (text == NULL || text[0] == '\0') {
74✔
965
        return;
966
    }
967
    if (sixel_decoder_threads_parse_value(text, &parsed)) {
71✔
968
        g_decoder_threads.env_threads = parsed;
71✔
969
        g_decoder_threads.env_valid = 1;
71✔
970
    }
971
}
972

973
SIXELSTATUS
974
sixel_decoder_parallel_override_threads(char const *text)
1✔
975
{
976
    SIXELSTATUS status;
1✔
977
    int parsed;
1✔
978

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

997
int
998
sixel_decoder_parallel_resolve_threads(void)
75✔
999
{
1000
    int threads;
75✔
1001

1002
    threads = 1;
75✔
1003
    sixel_decoder_threads_load_env();
75✔
1004
    if (g_decoder_threads.override_active) {
75✔
1005
        threads = sixel_decoder_threads_normalize(
×
1006
            g_decoder_threads.override_threads);
1007
    } else if (g_decoder_threads.env_valid) {
75✔
1008
        threads = sixel_decoder_threads_normalize(
72✔
1009
            g_decoder_threads.env_threads);
1010
    }
1011
    if (threads < 1) {
×
1012
        threads = 1;
1013
    }
1014
    return threads;
75✔
1015
}
1016

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

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

1066
    if (input == NULL || anchor == NULL || length <= 0 || image == NULL) {
75✔
1067
        runtime_error = 1;
×
1068
        goto cleanup;
×
1069
    }
1070

1071
    pixel_size = sixel_decoder_parallel_pixel_size(image->depth);
75✔
1072

1073
    payload_start = (int)(anchor - input);
75✔
1074
    if (payload_start < 0 || payload_start >= length) {
75✔
1075
        runtime_error = 1;
×
1076
        goto cleanup;
×
1077
    }
1078

1079
    payload_len = length - payload_start;
75✔
1080
    if (payload_len <= 0) {
75✔
1081
        runtime_error = 1;
1082
        goto cleanup;
1083
    }
1084

1085
    palette_limit = image->ncolors;
75✔
1086
    if (palette_limit <= 0 || palette_limit > SIXEL_PALETTE_MAX_DECODER) {
75✔
1087
        palette_limit = SIXEL_PALETTE_MAX_DECODER;
×
1088
    }
1089

1090
    threads = sixel_decoder_parallel_resolve_threads();
75✔
1091
    if (threads < 1) {
75✔
1092
        threads = 1;
1093
    }
1094
    if (threads > payload_len) {
75✔
1095
        threads = payload_len;
1096
    }
1097
    if (threads < 2) {
75✔
1098
        status = SIXEL_FALSE;
72✔
1099
        goto cleanup;
72✔
1100
    }
1101

1102
    workers = (sixel_thread_t *)calloc((size_t)threads,
3✔
1103
                                       sizeof(sixel_thread_t));
1104
    contexts = (sixel_decoder_worker_context_t *)calloc(
3✔
1105
        (size_t)threads, sizeof(sixel_decoder_worker_context_t));
1106
    spans = (int *)calloc((size_t)threads, sizeof(int));
3✔
1107
    copy_offsets = (int *)calloc((size_t)threads, sizeof(int));
3✔
1108
    copy_ready = (int *)calloc((size_t)threads, sizeof(int));
3✔
1109
    if (workers == NULL || contexts == NULL || spans == NULL ||
3✔
1110
            copy_offsets == NULL || copy_ready == NULL) {
3✔
1111
        runtime_error = 1;
×
1112
        goto cleanup;
×
1113
    }
1114

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

1132
    sixel_decoder_parallel_fill_spans(payload_len, threads, spans);
3✔
1133

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

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

1196
    for (i = 0; i < created; ++i) {
15✔
1197
        sixel_thread_join(&workers[i]);
12✔
1198
        if (contexts[i].result != 0) {
12✔
1199
            parallel_failed = 1;
12✔
1200
        }
1201
    }
1202

1203
    if (chain.abort_requested) {
3✔
1204
        parallel_failed = 1;
3✔
1205
    }
1206

1207
    if (runtime_error || created < threads) {
3✔
1208
        status = SIXEL_RUNTIME_ERROR;
1209
    } else if (parallel_failed) {
3✔
1210
        status = SIXEL_FALSE;
1211
    } else {
1212
        status = SIXEL_OK;
1213
    }
1214

1215
cleanup:
1216
    if (sync_ready) {
72✔
1217
        sixel_mutex_destroy(&chain.mutex);
3✔
1218
        sixel_cond_destroy(&chain.cond);
3✔
1219
    }
1220

1221
    free(workers);
75✔
1222
    free(contexts);
75✔
1223
    free(spans);
75✔
1224
    free(copy_offsets);
75✔
1225
    free(copy_ready);
75✔
1226

1227
    return status;
75✔
1228
#else
1229
    (void)direct_mode;
1230
    (void)input;
1231
    (void)length;
1232
    (void)anchor;
1233
    (void)image;
1234
    (void)palette;
1235
    (void)logger;
1236

1237
    return SIXEL_RUNTIME_ERROR;
1238
#endif
1239
}
1240

1241
/* emacs Local Variables:      */
1242
/* emacs mode: c               */
1243
/* emacs tab-width: 4          */
1244
/* emacs indent-tabs-mode: nil */
1245
/* emacs c-basic-offset: 4     */
1246
/* emacs End:                  */
1247
/* vim: set expandtab ts=4 sts=4 sw=4 : */
1248
/* 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