mirror of
https://github.com/italicsjenga/vello.git
synced 2025-01-10 20:51:29 +11:00
307bf8d227
Adds a test to visualize the blend modes. Fixes a dumb bug in blend.h and also a more subtle issue where default blending is not the same as clipping, as the former needs to always push a blend group (to cause isolation) and the latter does not. This might be something we need to get back to. This should fix the rendering, so it fairly closely resembles the Mozilla reference image. There's also a compile-time switch to disable sRGB conversion, which is (sadly) needed for compatible rendering.
279 lines
11 KiB
GLSL
279 lines
11 KiB
GLSL
// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense
|
|
|
|
// This is "kernel 4" in a 4-kernel pipeline. It renders the commands
|
|
// in the per-tile command list to an image.
|
|
|
|
// Right now, this kernel stores the image in a buffer, but a better
|
|
// plan is to use a texture. This is because of limited support.
|
|
|
|
#version 450
|
|
#extension GL_GOOGLE_include_directive : enable
|
|
|
|
// We can do rendering either in sRGB colorspace (for compatibility)
|
|
// or in a linear colorspace, with conversions to sRGB (which will give
|
|
// higher quality antialiasing among other things).
|
|
#define DO_SRGB_CONVERSION 0
|
|
|
|
#include "mem.h"
|
|
#include "setup.h"
|
|
|
|
#define CHUNK_X 2
|
|
#define CHUNK_Y 4
|
|
#define CHUNK (CHUNK_X * CHUNK_Y)
|
|
#define CHUNK_DX (TILE_WIDTH_PX / CHUNK_X)
|
|
#define CHUNK_DY (TILE_HEIGHT_PX / CHUNK_Y)
|
|
layout(local_size_x = CHUNK_DX, local_size_y = CHUNK_DY) in;
|
|
|
|
layout(set = 0, binding = 1) restrict readonly buffer ConfigBuf {
|
|
Config conf;
|
|
};
|
|
|
|
#ifdef GRAY
|
|
layout(r8, set = 0, binding = 2) uniform restrict writeonly image2D image;
|
|
#else
|
|
layout(rgba8, set = 0, binding = 2) uniform restrict writeonly image2D image;
|
|
#endif
|
|
|
|
layout(rgba8, set = 0, binding = 3) uniform restrict readonly image2D image_atlas;
|
|
|
|
layout(rgba8, set = 0, binding = 4) uniform restrict readonly image2D gradients;
|
|
|
|
#include "ptcl.h"
|
|
#include "tile.h"
|
|
#include "blend.h"
|
|
|
|
#define MAX_BLEND_STACK 128
|
|
mediump vec3 tosRGB(mediump vec3 rgb) {
|
|
#if DO_SRGB_CONVERSION
|
|
bvec3 cutoff = greaterThanEqual(rgb, vec3(0.0031308));
|
|
mediump vec3 below = vec3(12.92) * rgb;
|
|
mediump vec3 above = vec3(1.055) * pow(rgb, vec3(0.41666)) - vec3(0.055);
|
|
return mix(below, above, cutoff);
|
|
#else
|
|
return rgb;
|
|
#endif
|
|
}
|
|
|
|
mediump vec3 fromsRGB(mediump vec3 srgb) {
|
|
#if DO_SRGB_CONVERSION
|
|
// Formula from EXT_sRGB.
|
|
bvec3 cutoff = greaterThanEqual(srgb, vec3(0.04045));
|
|
mediump vec3 below = srgb / vec3(12.92);
|
|
mediump vec3 above = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4));
|
|
return mix(below, above, cutoff);
|
|
#else
|
|
return srgb;
|
|
#endif
|
|
}
|
|
|
|
// unpacksRGB unpacks a color in the sRGB color space to a vec4 in the linear color
|
|
// space.
|
|
mediump vec4 unpacksRGB(uint srgba) {
|
|
mediump vec4 color = unpackUnorm4x8(srgba).wzyx;
|
|
return vec4(fromsRGB(color.rgb), color.a);
|
|
}
|
|
|
|
// packsRGB packs a color in the linear color space into its 8-bit sRGB equivalent.
|
|
uint packsRGB(mediump vec4 rgba) {
|
|
rgba = vec4(tosRGB(rgba.rgb), rgba.a);
|
|
return packUnorm4x8(rgba.wzyx);
|
|
}
|
|
|
|
uvec2 chunk_offset(uint i) {
|
|
return uvec2(i % CHUNK_X * CHUNK_DX, i / CHUNK_X * CHUNK_DY);
|
|
}
|
|
|
|
mediump vec4[CHUNK] fillImage(uvec2 xy, CmdImage cmd_img) {
|
|
mediump vec4 rgba[CHUNK];
|
|
for (uint i = 0; i < CHUNK; i++) {
|
|
ivec2 uv = ivec2(xy + chunk_offset(i)) + cmd_img.offset;
|
|
mediump vec4 fg_rgba;
|
|
fg_rgba = imageLoad(image_atlas, uv);
|
|
fg_rgba.rgb = fromsRGB(fg_rgba.rgb);
|
|
rgba[i] = fg_rgba;
|
|
}
|
|
return rgba;
|
|
}
|
|
|
|
void main() {
|
|
uint tile_ix = gl_WorkGroupID.y * conf.width_in_tiles + gl_WorkGroupID.x;
|
|
Alloc cmd_alloc = slice_mem(conf.ptcl_alloc, tile_ix * PTCL_INITIAL_ALLOC, PTCL_INITIAL_ALLOC);
|
|
CmdRef cmd_ref = CmdRef(cmd_alloc.offset);
|
|
|
|
uvec2 xy_uint = uvec2(gl_LocalInvocationID.x + TILE_WIDTH_PX * gl_WorkGroupID.x,
|
|
gl_LocalInvocationID.y + TILE_HEIGHT_PX * gl_WorkGroupID.y);
|
|
vec2 xy = vec2(xy_uint);
|
|
mediump vec4 rgba[CHUNK];
|
|
uint blend_stack[MAX_BLEND_STACK][CHUNK];
|
|
for (uint i = 0; i < CHUNK; i++) {
|
|
rgba[i] = vec4(0.0);
|
|
}
|
|
|
|
mediump float area[CHUNK];
|
|
uint clip_depth = 0;
|
|
bool mem_ok = mem_error == NO_ERROR;
|
|
while (mem_ok) {
|
|
uint tag = Cmd_tag(cmd_alloc, cmd_ref).tag;
|
|
if (tag == Cmd_End) {
|
|
break;
|
|
}
|
|
switch (tag) {
|
|
case Cmd_Stroke:
|
|
// Calculate distance field from all the line segments in this tile.
|
|
CmdStroke stroke = Cmd_Stroke_read(cmd_alloc, cmd_ref);
|
|
mediump float df[CHUNK];
|
|
for (uint k = 0; k < CHUNK; k++)
|
|
df[k] = 1e9;
|
|
TileSegRef tile_seg_ref = TileSegRef(stroke.tile_ref);
|
|
do {
|
|
TileSeg seg = TileSeg_read(new_alloc(tile_seg_ref.offset, TileSeg_size, mem_ok), tile_seg_ref);
|
|
vec2 line_vec = seg.vector;
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
vec2 dpos = xy + vec2(0.5, 0.5) - seg.origin;
|
|
dpos += vec2(chunk_offset(k));
|
|
float t = clamp(dot(line_vec, dpos) / dot(line_vec, line_vec), 0.0, 1.0);
|
|
df[k] = min(df[k], length(line_vec * t - dpos));
|
|
}
|
|
tile_seg_ref = seg.next;
|
|
} while (tile_seg_ref.offset != 0);
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
area[k] = clamp(stroke.half_width + 0.5 - df[k], 0.0, 1.0);
|
|
}
|
|
cmd_ref.offset += 4 + CmdStroke_size;
|
|
break;
|
|
case Cmd_Fill:
|
|
CmdFill fill = Cmd_Fill_read(cmd_alloc, cmd_ref);
|
|
for (uint k = 0; k < CHUNK; k++)
|
|
area[k] = float(fill.backdrop);
|
|
tile_seg_ref = TileSegRef(fill.tile_ref);
|
|
// Calculate coverage based on backdrop + coverage of each line segment
|
|
do {
|
|
TileSeg seg = TileSeg_read(new_alloc(tile_seg_ref.offset, TileSeg_size, mem_ok), tile_seg_ref);
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
vec2 my_xy = xy + vec2(chunk_offset(k));
|
|
vec2 start = seg.origin - my_xy;
|
|
vec2 end = start + seg.vector;
|
|
vec2 window = clamp(vec2(start.y, end.y), 0.0, 1.0);
|
|
if (window.x != window.y) {
|
|
vec2 t = (window - start.y) / seg.vector.y;
|
|
vec2 xs = vec2(mix(start.x, end.x, t.x), mix(start.x, end.x, t.y));
|
|
float xmin = min(min(xs.x, xs.y), 1.0) - 1e-6;
|
|
float xmax = max(xs.x, xs.y);
|
|
float b = min(xmax, 1.0);
|
|
float c = max(b, 0.0);
|
|
float d = max(xmin, 0.0);
|
|
float a = (b + 0.5 * (d * d - c * c) - xmin) / (xmax - xmin);
|
|
area[k] += a * (window.x - window.y);
|
|
}
|
|
area[k] += sign(seg.vector.x) * clamp(my_xy.y - seg.y_edge + 1.0, 0.0, 1.0);
|
|
}
|
|
tile_seg_ref = seg.next;
|
|
} while (tile_seg_ref.offset != 0);
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
area[k] = min(abs(area[k]), 1.0);
|
|
}
|
|
cmd_ref.offset += 4 + CmdFill_size;
|
|
break;
|
|
case Cmd_Solid:
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
area[k] = 1.0;
|
|
}
|
|
cmd_ref.offset += 4;
|
|
break;
|
|
case Cmd_Alpha:
|
|
CmdAlpha alpha = Cmd_Alpha_read(cmd_alloc, cmd_ref);
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
area[k] = alpha.alpha;
|
|
}
|
|
cmd_ref.offset += 4 + CmdAlpha_size;
|
|
break;
|
|
case Cmd_Color:
|
|
CmdColor color = Cmd_Color_read(cmd_alloc, cmd_ref);
|
|
mediump vec4 fg = unpacksRGB(color.rgba_color);
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
mediump vec4 fg_k = fg * area[k];
|
|
rgba[k] = rgba[k] * (1.0 - fg_k.a) + fg_k;
|
|
}
|
|
cmd_ref.offset += 4 + CmdColor_size;
|
|
break;
|
|
case Cmd_LinGrad:
|
|
CmdLinGrad lin = Cmd_LinGrad_read(cmd_alloc, cmd_ref);
|
|
float d = lin.line_x * float(xy.x) + lin.line_y * float(xy.y) + lin.line_c;
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
vec2 chunk_xy = vec2(chunk_offset(k));
|
|
float my_d = d + lin.line_x * chunk_xy.x + lin.line_y * chunk_xy.y;
|
|
int x = int(round(clamp(my_d, 0.0, 1.0) * float(GRADIENT_WIDTH - 1)));
|
|
mediump vec4 fg_rgba = imageLoad(gradients, ivec2(x, int(lin.index)));
|
|
fg_rgba.rgb = fromsRGB(fg_rgba.rgb);
|
|
mediump vec4 fg_k = fg_rgba * area[k];
|
|
rgba[k] = rgba[k] * (1.0 - fg_k.a) + fg_k;
|
|
}
|
|
cmd_ref.offset += 4 + CmdLinGrad_size;
|
|
break;
|
|
case Cmd_RadGrad:
|
|
CmdRadGrad rad = Cmd_RadGrad_read(cmd_alloc, cmd_ref);
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
vec2 my_xy = xy + vec2(chunk_offset(k));
|
|
my_xy = rad.mat.xz * my_xy.x + rad.mat.yw * my_xy.y - rad.xlat;
|
|
float ba = dot(my_xy, rad.c1);
|
|
float ca = rad.ra * dot(my_xy, my_xy);
|
|
float t = sqrt(ba * ba + ca) - ba - rad.roff;
|
|
int x = int(round(clamp(t, 0.0, 1.0) * float(GRADIENT_WIDTH - 1)));
|
|
mediump vec4 fg_rgba = imageLoad(gradients, ivec2(x, int(rad.index)));
|
|
fg_rgba.rgb = fromsRGB(fg_rgba.rgb);
|
|
mediump vec4 fg_k = fg_rgba * area[k];
|
|
rgba[k] = rgba[k] * (1.0 - fg_k.a) + fg_k;
|
|
}
|
|
cmd_ref.offset += 4 + CmdRadGrad_size;
|
|
break;
|
|
case Cmd_Image:
|
|
CmdImage fill_img = Cmd_Image_read(cmd_alloc, cmd_ref);
|
|
mediump vec4 img[CHUNK] = fillImage(xy_uint, fill_img);
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
mediump vec4 fg_k = img[k] * area[k];
|
|
rgba[k] = rgba[k] * (1.0 - fg_k.a) + fg_k;
|
|
}
|
|
cmd_ref.offset += 4 + CmdImage_size;
|
|
break;
|
|
case Cmd_BeginClip:
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
// We reject any inputs that might overflow in render_ctx.rs.
|
|
// The following is a sanity check so we don't corrupt memory should there be malformed inputs.
|
|
uint d = min(clip_depth, MAX_BLEND_STACK - 1);
|
|
blend_stack[d][k] = packsRGB(vec4(rgba[k]));
|
|
rgba[k] = vec4(0.0);
|
|
}
|
|
clip_depth++;
|
|
cmd_ref.offset += 4;
|
|
break;
|
|
case Cmd_EndClip:
|
|
CmdEndClip end_clip = Cmd_EndClip_read(cmd_alloc, cmd_ref);
|
|
uint blend_mode = uint(end_clip.blend >> 8);
|
|
uint comp_mode = uint(end_clip.blend & 0xFF);
|
|
clip_depth--;
|
|
for (uint k = 0; k < CHUNK; k++) {
|
|
uint d = min(clip_depth, MAX_BLEND_STACK - 1);
|
|
mediump vec4 bg = unpacksRGB(blend_stack[d][k]);
|
|
mediump vec4 fg = rgba[k] * area[k];
|
|
rgba[k] = mix_blend_compose(bg, fg, end_clip.blend);
|
|
}
|
|
cmd_ref.offset += 4 + CmdEndClip_size;
|
|
break;
|
|
case Cmd_Jump:
|
|
cmd_ref = CmdRef(Cmd_Jump_read(cmd_alloc, cmd_ref).new_ref);
|
|
cmd_alloc.offset = cmd_ref.offset;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (uint i = 0; i < CHUNK; i++) {
|
|
#ifdef GRAY
|
|
// Just store the alpha value; later we can specialize this kernel more to avoid
|
|
// computing unneeded RGB colors.
|
|
imageStore(image, ivec2(xy_uint + chunk_offset(i)), vec4(rgba[i].a));
|
|
#else
|
|
imageStore(image, ivec2(xy_uint + chunk_offset(i)), vec4(tosRGB(rgba[i].rgb), rgba[i].a));
|
|
#endif
|
|
}
|
|
}
|