vello/piet-gpu/shader/pathseg.comp
Raph Levien acb3933d94 Variable size encoding of draw objects
This patch switches to a variable size encoding of draw objects.

In addition to the CPU-side scene encoding, it changes the representation of intermediate per draw object state from the `Annotated` struct to a variable "info" encoding. In addition, the bounding boxes are moved to a separate array (for a more "structure of "arrays" approach). Data that's unchanged from the scene encoding is not copied. Rather, downstream stages can access the data from the scene buffer (reducing allocation and copying).

Prefix sums, computed in `DrawMonoid` track the offset of both scene and intermediate data. The tags for the CPU-side encoding have been split into their own stream (again a change from AoS to SoA style).

This is not necessarily the final form. There's some stuff (including at least one piet-gpu-derive type) that can be deleted. In addition, the linewidth field should probably move from the info to path-specific. Also, the 1:1 correspondence between draw object and path has not yet been broken.

Closes #152
2022-03-14 16:32:08 -07:00

291 lines
10 KiB
GLSL

// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense
// Processing of the path stream, after the tag scan.
#version 450
#extension GL_GOOGLE_include_directive : enable
#include "mem.h"
#include "setup.h"
#include "pathtag.h"
#define N_SEQ 4
#define LG_WG_SIZE (7 + LG_WG_FACTOR)
#define WG_SIZE (1 << LG_WG_SIZE)
#define PARTITION_SIZE (WG_SIZE * N_SEQ)
layout(local_size_x = WG_SIZE, local_size_y = 1) in;
layout(binding = 1) readonly buffer ConfigBuf {
Config conf;
};
layout(binding = 2) readonly buffer SceneBuf {
uint[] scene;
};
#include "tile.h"
#include "pathseg.h"
layout(binding = 3) readonly buffer ParentBuf {
TagMonoid[] parent;
};
struct Monoid {
vec4 bbox;
uint flags;
};
#define FLAG_RESET_BBOX 1
#define FLAG_SET_BBOX 2
Monoid combine_monoid(Monoid a, Monoid b) {
Monoid c;
c.bbox = b.bbox;
// TODO: I think this should be gated on b & SET_BBOX == false also.
if ((a.flags & FLAG_RESET_BBOX) == 0 && b.bbox.z <= b.bbox.x && b.bbox.w <= b.bbox.y) {
c.bbox = a.bbox;
} else if ((a.flags & FLAG_RESET_BBOX) == 0 && (b.flags & FLAG_SET_BBOX) == 0 &&
(a.bbox.z > a.bbox.x || a.bbox.w > a.bbox.y)) {
c.bbox.xy = min(a.bbox.xy, c.bbox.xy);
c.bbox.zw = max(a.bbox.zw, c.bbox.zw);
}
c.flags = (a.flags & FLAG_SET_BBOX) | b.flags;
c.flags |= ((a.flags & FLAG_RESET_BBOX) << 1);
return c;
}
Monoid monoid_identity() {
return Monoid(vec4(0.0, 0.0, 0.0, 0.0), 0);
}
// These are not both live at the same time. A very smart shader compiler
// would be able to figure that out, but I suspect many won't.
shared TagMonoid sh_tag[WG_SIZE];
shared Monoid sh_scratch[WG_SIZE];
vec2 read_f32_point(uint ix) {
float x = uintBitsToFloat(scene[ix]);
float y = uintBitsToFloat(scene[ix + 1]);
return vec2(x, y);
}
vec2 read_i16_point(uint ix) {
uint raw = scene[ix];
float x = float(int(raw << 16) >> 16);
float y = float(int(raw) >> 16);
return vec2(x, y);
}
// Note: these are 16 bit, which is adequate, but we could use 32 bits.
// Round down and saturate to minimum integer; add bias
uint round_down(float x) {
return uint(max(0.0, floor(x) + 32768.0));
}
// Round up and saturate to maximum integer; add bias
uint round_up(float x) {
return uint(min(65535.0, ceil(x) + 32768.0));
}
void main() {
Monoid local[N_SEQ];
float linewidth[N_SEQ];
uint save_trans_ix[N_SEQ];
uint ix = gl_GlobalInvocationID.x * N_SEQ;
uint tag_word = scene[(conf.pathtag_offset >> 2) + (ix >> 2)];
// Scan the tag monoid
TagMonoid local_tm = reduce_tag(tag_word);
sh_tag[gl_LocalInvocationID.x] = local_tm;
for (uint i = 0; i < LG_WG_SIZE; i++) {
barrier();
if (gl_LocalInvocationID.x >= (1u << i)) {
TagMonoid other = sh_tag[gl_LocalInvocationID.x - (1u << i)];
local_tm = combine_tag_monoid(other, local_tm);
}
barrier();
sh_tag[gl_LocalInvocationID.x] = local_tm;
}
barrier();
// sh_tag is now the partition-wide inclusive scan of the tag monoid.
TagMonoid tm = tag_monoid_identity();
if (gl_WorkGroupID.x > 0) {
tm = parent[gl_WorkGroupID.x - 1];
}
if (gl_LocalInvocationID.x > 0) {
tm = combine_tag_monoid(tm, sh_tag[gl_LocalInvocationID.x - 1]);
}
// tm is now the full exclusive scan of the tag monoid.
// Indices to scene buffer in u32 units.
uint ps_ix = (conf.pathseg_offset >> 2) + tm.pathseg_offset;
uint lw_ix = (conf.linewidth_offset >> 2) + tm.linewidth_ix;
uint save_path_ix = tm.path_ix;
uint trans_ix = tm.trans_ix;
TransformSegRef trans_ref = TransformSegRef(conf.trans_alloc.offset + trans_ix * TransformSeg_size);
PathSegRef ps_ref = PathSegRef(conf.pathseg_alloc.offset + tm.pathseg_ix * PathSeg_size);
for (uint i = 0; i < N_SEQ; i++) {
linewidth[i] = uintBitsToFloat(scene[lw_ix]);
save_trans_ix[i] = trans_ix;
// if N_SEQ > 4, need to load tag_word from local if N_SEQ % 4 == 0
uint tag_byte = tag_word >> (i * 8);
uint seg_type = tag_byte & 3;
if (seg_type != 0) {
// 1 = line, 2 = quad, 3 = cubic
// Unpack path segment from input
vec2 p0;
vec2 p1;
vec2 p2;
vec2 p3;
if ((tag_byte & 8) != 0) {
// 32 bit encoding
p0 = read_f32_point(ps_ix);
p1 = read_f32_point(ps_ix + 2);
if (seg_type >= 2) {
p2 = read_f32_point(ps_ix + 4);
if (seg_type == 3) {
p3 = read_f32_point(ps_ix + 6);
}
}
} else {
// 16 bit encoding
p0 = read_i16_point(ps_ix);
p1 = read_i16_point(ps_ix + 1);
if (seg_type >= 2) {
p2 = read_i16_point(ps_ix + 2);
if (seg_type == 3) {
p3 = read_i16_point(ps_ix + 3);
}
}
}
TransformSeg transform = TransformSeg_read(conf.trans_alloc, trans_ref);
p0 = transform.mat.xy * p0.x + transform.mat.zw * p0.y + transform.translate;
p1 = transform.mat.xy * p1.x + transform.mat.zw * p1.y + transform.translate;
vec4 bbox = vec4(min(p0, p1), max(p0, p1));
// Degree-raise and compute bbox
if (seg_type >= 2) {
p2 = transform.mat.xy * p2.x + transform.mat.zw * p2.y + transform.translate;
bbox.xy = min(bbox.xy, p2);
bbox.zw = max(bbox.zw, p2);
if (seg_type == 3) {
p3 = transform.mat.xy * p3.x + transform.mat.zw * p3.y + transform.translate;
bbox.xy = min(bbox.xy, p3);
bbox.zw = max(bbox.zw, p3);
} else {
p3 = p2;
p2 = mix(p1, p2, 1.0 / 3.0);
p1 = mix(p1, p0, 1.0 / 3.0);
}
} else {
p3 = p1;
p2 = mix(p3, p0, 1.0 / 3.0);
p1 = mix(p0, p3, 1.0 / 3.0);
}
vec2 stroke = vec2(0.0, 0.0);
if (linewidth[i] >= 0.0) {
// See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm
stroke = 0.5 * linewidth[i] * vec2(length(transform.mat.xz), length(transform.mat.yw));
bbox += vec4(-stroke, stroke);
}
local[i].bbox = bbox;
local[i].flags = 0;
// Write path segment to output
PathCubic cubic;
cubic.p0 = p0;
cubic.p1 = p1;
cubic.p2 = p2;
cubic.p3 = p3;
cubic.path_ix = tm.path_ix;
// Not needed, TODO remove from struct
cubic.trans_ix = gl_GlobalInvocationID.x * 4 + i;
cubic.stroke = stroke;
uint fill_mode = uint(linewidth[i] >= 0.0);
PathSeg_Cubic_write(conf.pathseg_alloc, ps_ref, fill_mode, cubic);
ps_ref.offset += PathSeg_size;
uint n_points = (tag_byte & 3) + ((tag_byte >> 2) & 1);
uint n_words = n_points + (n_points & (((tag_byte >> 3) & 1) * 15));
ps_ix += n_words;
} else {
local[i].bbox = vec4(0.0, 0.0, 0.0, 0.0);
// These shifts need to be kept in sync with setup.h
uint is_path = (tag_byte >> 4) & 1;
// Relies on the fact that RESET_BBOX == 1
local[i].flags = is_path;
tm.path_ix += is_path;
trans_ix += (tag_byte >> 5) & 1;
trans_ref.offset += ((tag_byte >> 5) & 1) * TransformSeg_size;
lw_ix += (tag_byte >> 6) & 1;
}
}
// Partition-wide monoid scan for bbox monoid
Monoid agg = local[0];
for (uint i = 1; i < N_SEQ; i++) {
// Note: this could be fused with the map above, but probably
// a thin performance gain not worth the complexity.
agg = combine_monoid(agg, local[i]);
local[i] = agg;
}
// local is N_SEQ sub-partition inclusive scan of bbox monoid.
sh_scratch[gl_LocalInvocationID.x] = agg;
for (uint i = 0; i < LG_WG_SIZE; i++) {
barrier();
if (gl_LocalInvocationID.x >= (1u << i)) {
Monoid other = sh_scratch[gl_LocalInvocationID.x - (1u << i)];
agg = combine_monoid(other, agg);
}
barrier();
sh_scratch[gl_LocalInvocationID.x] = agg;
}
// sh_scratch is the partition-wide inclusive scan of the bbox monoid,
// sampled at the end of the N_SEQ sub-partition.
barrier();
uint path_ix = save_path_ix;
uint bbox_out_ix = (conf.path_bbox_alloc.offset >> 2) + path_ix * 6;
// Write bboxes to paths; do atomic min/max if partial
Monoid row = monoid_identity();
if (gl_LocalInvocationID.x > 0) {
row = sh_scratch[gl_LocalInvocationID.x - 1];
}
for (uint i = 0; i < N_SEQ; i++) {
Monoid m = combine_monoid(row, local[i]);
// m is partition-wide inclusive scan of bbox monoid.
bool do_atomic = false;
if (i == N_SEQ - 1 && gl_LocalInvocationID.x == WG_SIZE - 1) {
// last element
do_atomic = true;
}
if ((m.flags & FLAG_RESET_BBOX) != 0) {
memory[bbox_out_ix + 4] = floatBitsToUint(linewidth[i]);
memory[bbox_out_ix + 5] = save_trans_ix[i];
if ((m.flags & FLAG_SET_BBOX) == 0) {
do_atomic = true;
} else {
memory[bbox_out_ix] = round_down(m.bbox.x);
memory[bbox_out_ix + 1] = round_down(m.bbox.y);
memory[bbox_out_ix + 2] = round_up(m.bbox.z);
memory[bbox_out_ix + 3] = round_up(m.bbox.w);
bbox_out_ix += 6;
do_atomic = false;
}
}
if (do_atomic) {
if (m.bbox.z > m.bbox.x || m.bbox.w > m.bbox.y) {
// atomic min/max
atomicMin(memory[bbox_out_ix], round_down(m.bbox.x));
atomicMin(memory[bbox_out_ix + 1], round_down(m.bbox.y));
atomicMax(memory[bbox_out_ix + 2], round_up(m.bbox.z));
atomicMax(memory[bbox_out_ix + 3], round_up(m.bbox.w));
}
bbox_out_ix += 6;
}
}
}