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

saitoha / libsixel / 20612361446

31 Dec 2025 04:55AM UTC coverage: 51.955% (-0.06%) from 52.011%
20612361446

push

github

saitoha
tests: add missing helper shell tests/conversion/common.sh

14765 of 45141 branches covered (32.71%)

21477 of 41338 relevant lines covered (51.95%)

3912962.93 hits per line

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

76.8
/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)
9✔
129
{
130
    char const *text;
9✔
131
    char *endptr;
9✔
132
    long value;
9✔
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");
9✔
140
    if (text == NULL || text[0] == '\0') {
9!
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,
9✔
161
                                  int threads,
162
                                  int *spans)
163
{
164
    int base_share;
9✔
165
    int skew_percent;
9✔
166
    double skew;
9✔
167
    int i;
9✔
168
    double center;
9✔
169
    int total;
9✔
170
    int remainder;
9✔
171

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

185
    remainder = payload_len - total;
9✔
186
    while (remainder > 0) {
9!
187
        for (i = threads - 1; i >= 0 && remainder > 0; --i) {
×
188
            spans[i] += 1;
×
189
            --remainder;
×
190
        }
191
    }
192
    while (remainder < 0) {
9!
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
}
9✔
204

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

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

217
    return pixel_size;
219✔
218
}
219

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

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

233
    if (depth == 2) {
1,728!
234
        unsigned short packed;
×
235

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

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

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

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

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

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

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

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

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

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

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

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

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

322
    return chunk;
36✔
323
}
324

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

331
    cursor = buffer->cursor;
432✔
332
    while (cursor != NULL && row >= cursor->start_row + cursor->rows) {
432!
333
        cursor = cursor->next;
×
334
    }
335
    if (cursor != NULL && row >= cursor->start_row) {
432!
336
        buffer->cursor = cursor;
432✔
337
        return cursor;
432✔
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)
36✔
373
{
374
    sixel_decoder_worker_context_t *context;
36✔
375
    sixel_decoder_worker_chain_t *chain;
36✔
376
    sixel_local_buffer_t local_buffer;
36✔
377
    sixel_decoder_local_chunk_t *chunk_cursor = NULL;
36✔
378
    unsigned char *anchor = NULL;
36✔
379
    unsigned char *scan = NULL;
36✔
380
    unsigned char *cursor = NULL;
36✔
381
    unsigned char *stop = NULL;
36✔
382
    unsigned char *limit = NULL;
36✔
383
    unsigned char *start = NULL;
36✔
384
    int capacity = 0;
36✔
385
    int written = 0;
36✔
386
    int assigned = 0;
36✔
387
    int bits = 0;
36✔
388
    int repeat = 1;
36✔
389
    int color_index = 0;
36✔
390
    int max_relative = (-1);
36✔
391
    int min_relative = (-1);
36✔
392
    int pos_x = 0;
36✔
393
    int pos_y = 0;
36✔
394
    int relative = 0;
36✔
395
    int row_base = 0;
36✔
396
    int r = 0;
36✔
397
    int row_offset = 0;
36✔
398
    int line_count = 0;
36✔
399
    int copy_offset = 0;
36✔
400
    int copy_span = 0;
36✔
401
    int next_offset = 0;
36✔
402
    int i;
36✔
403
    int fallback = 0;
36✔
404
    int status = (-1);
36✔
405
    sixel_logger_t *logger = NULL;
36✔
406
    int width = 0;
36✔
407
    int pixel_size = 0;
36✔
408
    int depth = 0;
36✔
409
    int height = 0;
36✔
410
    int chain_offset = 0;
36✔
411
    unsigned char ch;
36✔
412

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

418
    context->result = status;
36✔
419

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

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

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

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

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

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

465
    status = 0;
36✔
466

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

507
    if (anchor != NULL && anchor < cursor) {
36!
508
        scan = anchor;
509
        while (scan < cursor) {
8,721✔
510
            if (*scan == '-') {
8,694✔
511
                line_count += 1;
27✔
512
                scan += 1;
27✔
513
                continue;
27✔
514
            }
515

516
            if (*scan == '#') {
8,667✔
517
                int value;
1,728✔
518
                unsigned char *p;
1,728✔
519

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

541
            scan += 1;
6,939✔
542
        }
543
    }
544
    row_offset = line_count * 6;
36✔
545

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

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

569
    stop = context->input + context->end_offset;
36✔
570
    limit = context->input + context->length;
36✔
571
    while (cursor < limit) {
378!
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;
378✔
577

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

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

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

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

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

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

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

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

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

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

690
        if (ch == '-') {
45!
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) {
117!
708
            if (ch == 0x18 || ch == 0x1a) {
135!
709
                fallback = 1;
710
                status = (-1);
711
                break;
712
            }
713
            cursor += 1;
135✔
714
            if (ch == 0x1b && cursor < limit && *cursor == '\\') {
135!
715
                status = (0);
716
                break;
717
            }
718
            continue;
108✔
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;
36✔
731
    if (copy_span < 0) {
36!
732
        copy_span = 0;
733
    }
734

735
    if (logger != NULL) {
36!
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) {
36✔
753
        sixel_mutex_lock(&chain->mutex);
9✔
754
        chain->abort_requested = 1;
9✔
755
        sixel_cond_broadcast(&chain->cond);
9✔
756
        sixel_mutex_unlock(&chain->mutex);
9✔
757
        sixel_local_buffer_dispose(&local_buffer);
9✔
758
        context->result = status;
9✔
759
        return status;
9✔
760
    }
761

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

767
    sixel_mutex_lock(&chain->mutex);
27✔
768
    while (!chain->copy_ready[context->index] && !chain->abort_requested) {
30!
769
        sixel_cond_wait(&chain->cond, &chain->mutex);
3✔
770
    }
771
    if (chain->abort_requested) {
27!
772
        sixel_mutex_unlock(&chain->mutex);
27✔
773
        sixel_local_buffer_dispose(&local_buffer);
27✔
774
        context->local_buffer = NULL;
27✔
775
        context->local_capacity = 0;
27✔
776
        context->local_written = 0;
27✔
777
        status = (-1);
27✔
778
        context->result = status;
27✔
779
        return status;
27✔
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)
210✔
880
{
881
    if (text == NULL) {
210!
882
        return 0;
883
    }
884
    if ((text[0] == 'a' || text[0] == 'A') &&
210!
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)
210✔
896
{
897
    int normalized;
210✔
898

899
#if SIXEL_ENABLE_THREADS
900
    int hw_threads;
210✔
901

902
    if (requested <= 0) {
210!
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) {
1!
916
        normalized = 1;
917
    }
918
    return normalized;
×
919
}
920

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

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

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

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

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

979
    status = SIXEL_BAD_ARGUMENT;
3✔
980
    if (text == NULL || text[0] == '\0') {
3!
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)) {
3!
986
        sixel_helper_set_additional_message(
3✔
987
            "decoder: threads must be a positive integer or 'auto'.");
988
        goto end;
3✔
989
    }
990
    g_decoder_threads.override_active = 1;
×
991
    g_decoder_threads.override_threads = parsed;
×
992
    status = SIXEL_OK;
×
993
end:
3✔
994
    return status;
3✔
995
}
996

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

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

1017
SIXELSTATUS
1018
sixel_decoder_parallel_request_start(int direct_mode,
219✔
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;
219✔
1028
    int threads;
219✔
1029
    int payload_start;
219✔
1030
    int payload_len;
219✔
1031
    sixel_thread_t *workers;
219✔
1032
    sixel_decoder_worker_context_t *contexts;
219✔
1033
    sixel_decoder_worker_chain_t chain;
219✔
1034
    int *copy_offsets;
219✔
1035
    int *copy_ready;
219✔
1036
    int global_capacity;
219✔
1037
    int *spans;
219✔
1038
    int i;
219✔
1039
    int offset;
219✔
1040
    int created;
219✔
1041
    int parallel_failed;
219✔
1042
    int runtime_error;
219✔
1043
    int sync_ready;
219✔
1044
    int pixel_size;
219✔
1045
    int palette_limit;
219✔
1046

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1227
    return status;
219✔
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

© 2025 Coveralls, Inc