Coverage Report

Created: 2025-09-04 07:15

/src/mpv/video/out/gpu/user_shaders.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * This file is part of mpv.
3
 *
4
 * mpv is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU Lesser General Public
6
 * License as published by the Free Software Foundation; either
7
 * version 2.1 of the License, or (at your option) any later version.
8
 *
9
 * mpv is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public
15
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
#include <assert.h>
19
#include <math.h>
20
21
#include "common/msg.h"
22
#include "misc/ctype.h"
23
#include "user_shaders.h"
24
25
static bool parse_rpn_szexpr(struct bstr line, struct szexp out[MAX_SZEXP_SIZE])
26
0
{
27
0
    int pos = 0;
28
29
0
    while (line.len > 0) {
30
0
        struct bstr word = bstr_strip(bstr_splitchar(line, &line, ' '));
31
0
        if (word.len == 0)
32
0
            continue;
33
34
0
        if (pos >= MAX_SZEXP_SIZE)
35
0
            return false;
36
37
0
        struct szexp *exp = &out[pos++];
38
39
0
        if (bstr_eatend0(&word, ".w") || bstr_eatend0(&word, ".width")) {
40
0
            exp->tag = SZEXP_VAR_W;
41
0
            exp->val.varname = word;
42
0
            continue;
43
0
        }
44
45
0
        if (bstr_eatend0(&word, ".h") || bstr_eatend0(&word, ".height")) {
46
0
            exp->tag = SZEXP_VAR_H;
47
0
            exp->val.varname = word;
48
0
            continue;
49
0
        }
50
51
0
        switch (word.start[0]) {
52
0
        case '+': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_ADD; continue;
53
0
        case '-': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_SUB; continue;
54
0
        case '*': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_MUL; continue;
55
0
        case '/': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_DIV; continue;
56
0
        case '%': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_MOD; continue;
57
0
        case '!': exp->tag = SZEXP_OP1; exp->val.op = SZEXP_OP_NOT; continue;
58
0
        case '>': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_GT;  continue;
59
0
        case '<': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_LT;  continue;
60
0
        case '=': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_EQ;  continue;
61
0
        }
62
63
0
        if (mp_isdigit(word.start[0])) {
64
0
            exp->tag = SZEXP_CONST;
65
0
            if (bstr_sscanf(word, "%f", &exp->val.cval) != 1)
66
0
                return false;
67
0
            continue;
68
0
        }
69
70
        // Some sort of illegal expression
71
0
        return false;
72
0
    }
73
74
0
    return true;
75
0
}
76
77
// Returns whether successful. 'result' is left untouched on failure
78
bool eval_szexpr(struct mp_log *log, void *priv,
79
                 bool (*lookup)(void *priv, struct bstr var, float size[2]),
80
                 struct szexp expr[MAX_SZEXP_SIZE], float *result)
81
0
{
82
0
    float stack[MAX_SZEXP_SIZE] = {0};
83
0
    int idx = 0; // points to next element to push
84
85
0
    for (int i = 0; i < MAX_SZEXP_SIZE; i++) {
86
0
        switch (expr[i].tag) {
87
0
        case SZEXP_END:
88
0
            goto done;
89
90
0
        case SZEXP_CONST:
91
            // Since our SZEXPs are bound by MAX_SZEXP_SIZE, it should be
92
            // impossible to overflow the stack
93
0
            mp_assert(idx < MAX_SZEXP_SIZE);
94
0
            stack[idx++] = expr[i].val.cval;
95
0
            continue;
96
97
0
        case SZEXP_OP1:
98
0
            if (idx < 1) {
99
0
                mp_warn(log, "Stack underflow in RPN expression!\n");
100
0
                return false;
101
0
            }
102
103
0
            switch (expr[i].val.op) {
104
0
            case SZEXP_OP_NOT: stack[idx-1] = !stack[idx-1]; break;
105
0
            default: MP_ASSERT_UNREACHABLE();
106
0
            }
107
0
            continue;
108
109
0
        case SZEXP_OP2:
110
0
            if (idx < 2) {
111
0
                mp_warn(log, "Stack underflow in RPN expression!\n");
112
0
                return false;
113
0
            }
114
115
            // Pop the operands in reverse order
116
0
            float op2 = stack[--idx];
117
0
            float op1 = stack[--idx];
118
0
            float res = 0.0;
119
0
            switch (expr[i].val.op) {
120
0
            case SZEXP_OP_ADD: res = op1 + op2; break;
121
0
            case SZEXP_OP_SUB: res = op1 - op2; break;
122
0
            case SZEXP_OP_MUL: res = op1 * op2; break;
123
0
            case SZEXP_OP_DIV: res = op1 / op2; break;
124
0
            case SZEXP_OP_MOD: res = fmodf(op1, op2); break;
125
0
            case SZEXP_OP_GT:  res = op1 > op2; break;
126
0
            case SZEXP_OP_LT:  res = op1 < op2; break;
127
0
            case SZEXP_OP_EQ:  res = op1 == op2; break;
128
0
            default: MP_ASSERT_UNREACHABLE();
129
0
            }
130
131
0
            if (!isfinite(res)) {
132
0
                mp_warn(log, "Illegal operation in RPN expression!\n");
133
0
                return false;
134
0
            }
135
136
0
            stack[idx++] = res;
137
0
            continue;
138
139
0
        case SZEXP_VAR_W:
140
0
        case SZEXP_VAR_H: {
141
0
            struct bstr name = expr[i].val.varname;
142
0
            float size[2];
143
144
0
            if (!lookup(priv, name, size)) {
145
0
                mp_warn(log, "Variable %.*s not found in RPN expression!\n",
146
0
                        BSTR_P(name));
147
0
                return false;
148
0
            }
149
150
0
            stack[idx++] = (expr[i].tag == SZEXP_VAR_W) ? size[0] : size[1];
151
0
            continue;
152
0
            }
153
0
        }
154
0
    }
155
156
0
done:
157
    // Return the single stack element
158
0
    if (idx != 1) {
159
0
        mp_warn(log, "Malformed stack after RPN expression!\n");
160
0
        return false;
161
0
    }
162
163
0
    *result = stack[0];
164
0
    return true;
165
0
}
166
167
static bool parse_hook(struct mp_log *log, struct bstr *body,
168
                       struct gl_user_shader_hook *out)
169
0
{
170
0
    *out = (struct gl_user_shader_hook){
171
0
        .pass_desc = bstr0("(unknown)"),
172
0
        .offset = identity_trans,
173
0
        .align_offset = false,
174
0
        .width = {{ SZEXP_VAR_W, { .varname = bstr0("HOOKED") }}},
175
0
        .height = {{ SZEXP_VAR_H, { .varname = bstr0("HOOKED") }}},
176
0
        .cond = {{ SZEXP_CONST, { .cval = 1.0 }}},
177
0
    };
178
179
0
    int hook_idx = 0;
180
0
    int bind_idx = 0;
181
182
    // Parse all headers
183
0
    while (true) {
184
0
        struct bstr rest;
185
0
        struct bstr line = bstr_strip(bstr_getline(*body, &rest));
186
187
        // Check for the presence of the magic line beginning
188
0
        if (!bstr_eatstart0(&line, "//!"))
189
0
            break;
190
191
0
        *body = rest;
192
193
        // Parse the supported commands
194
0
        if (bstr_eatstart0(&line, "HOOK")) {
195
0
            if (hook_idx == SHADER_MAX_HOOKS) {
196
0
                mp_err(log, "Passes may only hook up to %d textures!\n",
197
0
                       SHADER_MAX_HOOKS);
198
0
                return false;
199
0
            }
200
0
            out->hook_tex[hook_idx++] = bstr_strip(line);
201
0
            continue;
202
0
        }
203
204
0
        if (bstr_eatstart0(&line, "BIND")) {
205
0
            if (bind_idx == SHADER_MAX_BINDS) {
206
0
                mp_err(log, "Passes may only bind up to %d textures!\n",
207
0
                       SHADER_MAX_BINDS);
208
0
                return false;
209
0
            }
210
0
            out->bind_tex[bind_idx++] = bstr_strip(line);
211
0
            continue;
212
0
        }
213
214
0
        if (bstr_eatstart0(&line, "SAVE")) {
215
0
            out->save_tex = bstr_strip(line);
216
0
            continue;
217
0
        }
218
219
0
        if (bstr_eatstart0(&line, "DESC")) {
220
0
            out->pass_desc = bstr_strip(line);
221
0
            continue;
222
0
        }
223
224
0
        if (bstr_eatstart0(&line, "OFFSET")) {
225
0
            line = bstr_strip(line);
226
0
            if (bstr_equals0(line, "ALIGN")) {
227
0
                out->align_offset = true;
228
0
            } else {
229
0
                float ox, oy;
230
0
                if (bstr_sscanf(line, "%f %f", &ox, &oy) != 2) {
231
0
                    mp_err(log, "Error while parsing OFFSET!\n");
232
0
                    return false;
233
0
                }
234
0
                out->offset.t[0] = ox;
235
0
                out->offset.t[1] = oy;
236
0
            }
237
0
            continue;
238
0
        }
239
240
0
        if (bstr_eatstart0(&line, "WIDTH")) {
241
0
            if (!parse_rpn_szexpr(line, out->width)) {
242
0
                mp_err(log, "Error while parsing WIDTH!\n");
243
0
                return false;
244
0
            }
245
0
            continue;
246
0
        }
247
248
0
        if (bstr_eatstart0(&line, "HEIGHT")) {
249
0
            if (!parse_rpn_szexpr(line, out->height)) {
250
0
                mp_err(log, "Error while parsing HEIGHT!\n");
251
0
                return false;
252
0
            }
253
0
            continue;
254
0
        }
255
256
0
        if (bstr_eatstart0(&line, "WHEN")) {
257
0
            if (!parse_rpn_szexpr(line, out->cond)) {
258
0
                mp_err(log, "Error while parsing WHEN!\n");
259
0
                return false;
260
0
            }
261
0
            continue;
262
0
        }
263
264
0
        if (bstr_eatstart0(&line, "COMPONENTS")) {
265
0
            if (bstr_sscanf(line, "%d", &out->components) != 1) {
266
0
                mp_err(log, "Error while parsing COMPONENTS!\n");
267
0
                return false;
268
0
            }
269
0
            continue;
270
0
        }
271
272
0
        if (bstr_eatstart0(&line, "COMPUTE")) {
273
0
            struct compute_info *ci = &out->compute;
274
0
            int num = bstr_sscanf(line, "%d %d %d %d", &ci->block_w, &ci->block_h,
275
0
                                  &ci->threads_w, &ci->threads_h);
276
277
0
            if (num == 2 || num == 4) {
278
0
                ci->active = true;
279
0
                ci->directly_writes = true;
280
0
            } else {
281
0
                mp_err(log, "Error while parsing COMPUTE!\n");
282
0
                return false;
283
0
            }
284
0
            continue;
285
0
        }
286
287
        // Unknown command type
288
0
        mp_err(log, "Unrecognized command '%.*s'!\n", BSTR_P(line));
289
0
        return false;
290
0
    }
291
292
    // The rest of the file up until the next magic line beginning (if any)
293
    // shall be the shader body
294
0
    if (bstr_split_tok(*body, "//!", &out->pass_body, body)) {
295
        // Make sure the magic line is part of the rest
296
0
        body->start -= 3;
297
0
        body->len += 3;
298
0
    }
299
300
    // Sanity checking
301
0
    if (hook_idx == 0)
302
0
        mp_warn(log, "Pass has no hooked textures (will be ignored)!\n");
303
304
0
    return true;
305
0
}
306
307
static bool parse_tex(struct mp_log *log, struct ra *ra, struct bstr *body,
308
                      struct gl_user_shader_tex *out)
309
0
{
310
0
    *out = (struct gl_user_shader_tex){
311
0
        .name = bstr0("USER_TEX"),
312
0
        .params = {
313
0
            .dimensions = 2,
314
0
            .w = 1, .h = 1, .d = 1,
315
0
            .render_src = true,
316
0
            .src_linear = true,
317
0
        },
318
0
    };
319
0
    struct ra_tex_params *p = &out->params;
320
321
0
    while (true) {
322
0
        struct bstr rest;
323
0
        struct bstr line = bstr_strip(bstr_getline(*body, &rest));
324
325
0
        if (!bstr_eatstart0(&line, "//!"))
326
0
            break;
327
328
0
        *body = rest;
329
330
0
        if (bstr_eatstart0(&line, "TEXTURE")) {
331
0
            out->name = bstr_strip(line);
332
0
            continue;
333
0
        }
334
335
0
        if (bstr_eatstart0(&line, "SIZE")) {
336
0
            p->dimensions = bstr_sscanf(line, "%d %d %d", &p->w, &p->h, &p->d);
337
0
            if (p->dimensions < 1 || p->dimensions > 3 ||
338
0
                p->w < 1 || p->h < 1 || p->d < 1)
339
0
            {
340
0
                mp_err(log, "Error while parsing SIZE!\n");
341
0
                return false;
342
0
            }
343
0
            continue;
344
0
        }
345
346
0
        if (bstr_eatstart0(&line, "FORMAT ")) {
347
0
            p->format = NULL;
348
0
            for (int n = 0; n < ra->num_formats; n++) {
349
0
                const struct ra_format *fmt = ra->formats[n];
350
0
                if (bstr_equals0(line, fmt->name)) {
351
0
                    p->format = fmt;
352
0
                    break;
353
0
                }
354
0
            }
355
            // (pixel_size==0 is for opaque formats)
356
0
            if (!p->format || !p->format->pixel_size) {
357
0
                mp_err(log, "Unrecognized/unavailable FORMAT name: '%.*s'!\n",
358
0
                       BSTR_P(line));
359
0
                return false;
360
0
            }
361
0
            continue;
362
0
        }
363
364
0
        if (bstr_eatstart0(&line, "FILTER")) {
365
0
            line = bstr_strip(line);
366
0
            if (bstr_equals0(line, "LINEAR")) {
367
0
                p->src_linear = true;
368
0
            } else if (bstr_equals0(line, "NEAREST")) {
369
0
                p->src_linear = false;
370
0
            } else {
371
0
                mp_err(log, "Unrecognized FILTER: '%.*s'!\n", BSTR_P(line));
372
0
                return false;
373
0
            }
374
0
            continue;
375
0
        }
376
377
0
        if (bstr_eatstart0(&line, "BORDER")) {
378
0
            line = bstr_strip(line);
379
0
            if (bstr_equals0(line, "CLAMP")) {
380
0
                p->src_repeat = false;
381
0
            } else if (bstr_equals0(line, "REPEAT")) {
382
0
                p->src_repeat = true;
383
0
            } else {
384
0
                mp_err(log, "Unrecognized BORDER: '%.*s'!\n", BSTR_P(line));
385
0
                return false;
386
0
            }
387
0
            continue;
388
0
        }
389
390
0
        mp_err(log, "Unrecognized command '%.*s'!\n", BSTR_P(line));
391
0
        return false;
392
0
    }
393
394
0
    if (!p->format) {
395
0
        mp_err(log, "No FORMAT specified.\n");
396
0
        return false;
397
0
    }
398
399
0
    if (p->src_linear && !p->format->linear_filter) {
400
0
        mp_err(log, "The specified texture format cannot be filtered!\n");
401
0
        return false;
402
0
    }
403
404
    // Decode the rest of the section (up to the next //! marker) as raw hex
405
    // data for the texture
406
0
    struct bstr hexdata;
407
0
    if (bstr_split_tok(*body, "//!", &hexdata, body)) {
408
        // Make sure the magic line is part of the rest
409
0
        body->start -= 3;
410
0
        body->len += 3;
411
0
    }
412
413
0
    struct bstr tex;
414
0
    if (!bstr_decode_hex(NULL, bstr_strip(hexdata), &tex)) {
415
0
        mp_err(log, "Error while parsing TEXTURE body: must be a valid "
416
0
                    "hexadecimal sequence, on a single line!\n");
417
0
        return false;
418
0
    }
419
420
0
    int expected_len = p->w * p->h * p->d * p->format->pixel_size;
421
0
    if (tex.len != expected_len) {
422
0
        mp_err(log, "Shader TEXTURE size mismatch: got %zd bytes, expected %d!\n",
423
0
               tex.len, expected_len);
424
0
        talloc_free(tex.start);
425
0
        return false;
426
0
    }
427
428
0
    p->initial_data = tex.start;
429
0
    return true;
430
0
}
431
432
void parse_user_shader(struct mp_log *log, struct ra *ra, struct bstr shader,
433
                       void *priv,
434
                       bool (*dohook)(void *p, const struct gl_user_shader_hook *hook),
435
                       bool (*dotex)(void *p, struct gl_user_shader_tex tex))
436
0
{
437
0
    if (!dohook || !dotex || !shader.len)
438
0
        return;
439
440
    // Skip all garbage (e.g. comments) before the first header
441
0
    int pos = bstr_find(shader, bstr0("//!"));
442
0
    if (pos < 0) {
443
0
        mp_warn(log, "Shader appears to contain no headers!\n");
444
0
        return;
445
0
    }
446
0
    shader = bstr_cut(shader, pos);
447
448
    // Loop over the file
449
0
    while (shader.len > 0)
450
0
    {
451
        // Peek at the first header to dispatch the right type
452
0
        if (bstr_startswith0(shader, "//!TEXTURE")) {
453
0
            struct gl_user_shader_tex t;
454
0
            if (!parse_tex(log, ra, &shader, &t) || !dotex(priv, t))
455
0
                return;
456
0
            continue;
457
0
        }
458
459
0
        struct gl_user_shader_hook h;
460
0
        if (!parse_hook(log, &shader, &h) || !dohook(priv, &h))
461
0
            return;
462
0
    }
463
0
}