Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/spec/gen-rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ export function generateRust(): string {
// --- drawlist ------------------------------------------------------------------
put("/// DrawList op codes (core -> backend Vec<u32> words; layout in spec.ts).");
put("/// Word counts incl. header: RECT 4, GRAD_RECT 6, GLYPH_RUN 3+2n,");
put("/// TEX_QUAD 9, SCISSOR 3, SCISSOR_POP 1, TRI 7.");
put("/// TEX_QUAD 9, SCISSOR 3, SCISSOR_POP 1, TRI 7, TEX_TRI 12, TEXT_RUN 8+n, POLY 3+N.");
put("pub mod draw_op {");
for (const [name, v] of Object.entries(DRAW_OP)) {
put(` pub const ${screaming(name)}: u32 = ${v};`);
Expand Down
18 changes: 15 additions & 3 deletions contracts/spec/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1354,9 +1354,11 @@ export const FONT_FLAG_BOLD = 1 << 0;
// TRI (7 words): op, xy0, xy1, xy2, color0, color1, color2 — one
// CPU-clipped screen-space triangle (gouraud when the
// corner colors differ, flat otherwise). The core
// emits these only for ROTATED solid/gradient boxes
// after Sutherland-Hodgman clipping; axis-aligned
// content always uses RECT/GRAD_RECT.
// emits these for ROTATED gradient boxes after
// Sutherland-Hodgman clipping, and as a fan when a
// clipped polygon somehow exceeds 8 vertices.
// Axis-aligned content always uses RECT/GRAD_RECT;
// ROTATED solid boxes use POLY.
// TEX_TRI (12 words): op, texHandle, then 3 x { xy, u, v } (u/v = f32
// bits, normalized 0..1), color (modulate;
// 0xFFFFFFFF = none). One CPU-clipped textured
Expand All @@ -1370,6 +1372,15 @@ export const FONT_FLAG_BOLD = 1 << 0;
// perspective variation (projectively correct UVs
// at every cell corner), so interior texture lines
// do not kink at triangle diagonals.
// POLY (3 + N): op, N (3..=8), color, then N x xy — one
// CPU-clipped screen-space convex polygon, one flat
// colour, vertices CCW after raster setup. The core
// emits these for ROTATED solid boxes and for
// projected 3D faces after Sutherland-Hodgman
// clipping. Coverage is 4×4 samples over the whole
// polygon (interior run + boundary pixels) so a
// box's shared diagonal is not an interior edge.
// N > 8 falls back to a TRI fan.
// TEXT_RUN (8 + ceil(n/4) words):
// op,
// word1: bits 0-7 fontSlot,
Expand Down Expand Up @@ -1405,6 +1416,7 @@ export const DRAW_OP = {
tri: 7,
texTri: 8,
textRun: 9,
poly: 10,
} as const;

// ---------------------------------------------------------------------------
Expand Down
41 changes: 41 additions & 0 deletions engine/backends/esp32p4-ppa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,23 @@ impl Renderer {
}
i += 7;
}
spec::draw_op::POLY if i + 3 <= words.len() => {
let n = words[i + 1] as usize;
if !(3..=8).contains(&n) || i + 3 + n > words.len() {
return None;
}
if !polygon_bounds(&words[i + 3..i + 3 + n], clip).is_empty() {
self.software_op(
ui,
destination,
surface,
clip,
&words[i..i + 3 + n],
stats,
);
}
i += 3 + n;
}
spec::draw_op::TEX_TRI if i + 12 <= words.len() => {
if !triangle_bounds([words[i + 2], words[i + 5], words[i + 8]], clip).is_empty()
{
Expand Down Expand Up @@ -970,6 +987,30 @@ fn triangle_bounds(vertices: [u32; 3], clip: Clip) -> Clip {
.intersect(clip)
}

fn polygon_bounds(vertices: &[u32], clip: Clip) -> Clip {
if vertices.is_empty() {
return Clip::empty();
}
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
for &word in vertices {
let (x, y) = xy(word);
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
Clip {
x0: min_x,
y0: min_y,
x1: max_x,
y1: max_y,
}
.intersect(clip)
}

#[inline]
fn xy(word: u32) -> (i32, i32) {
(
Expand Down
124 changes: 86 additions & 38 deletions engine/backends/gpui/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ impl GpuiRenderer {
*i += 1;
return;
}
spec::draw_op::TRI | spec::draw_op::TEX_TRI => {
spec::draw_op::TRI | spec::draw_op::TEX_TRI | spec::draw_op::POLY => {
self.paint_tri_batch(ui, words, i, origin, window, cx);
}
spec::draw_op::TEXT_RUN => {
Expand Down Expand Up @@ -493,10 +493,11 @@ impl GpuiRenderer {

// ---- triangle batches (raster fallback) -----------------------------------

/// Paint one consecutive TRI/TEX_TRI batch starting at `*i`. Flat solid
/// TRIs alone stay vector paths; any gouraud or textured member sends
/// the WHOLE batch through the core software rasterizer so painter
/// order inside the batch (3D subtrees sort by depth) is preserved.
/// Paint one consecutive TRI/TEX_TRI/POLY batch starting at `*i`. Flat
/// solid TRIs and POLYs stay vector paths; any gouraud or textured
/// member sends the WHOLE batch through the core software rasterizer so
/// painter order inside the batch (3D subtrees sort by depth) is
/// preserved. POLY is flat-coloured by construction.
fn paint_tri_batch(
&mut self,
ui: &Ui,
Expand All @@ -521,21 +522,50 @@ impl GpuiRenderer {
needs_raster = true;
end += 12;
}
spec::draw_op::POLY => {
if end + 3 > words.len() {
break;
}
let n = words[end + 1] as usize;
if !(3..=8).contains(&n) || end + 3 + n > words.len() {
break;
}
end += 3 + n;
}
_ => break,
}
}
*i = end;
let batch = &words[start..end];
if !needs_raster {
for tri in batch.as_chunks::<7>().0 {
let color = abgr(tri[4]);
let (x0, y0) = decode_xy(tri[1]);
let (x1, y1) = decode_xy(tri[2]);
let (x2, y2) = decode_xy(tri[3]);
let mut path = Path::new(point(px(x0) + origin.x, px(y0) + origin.y));
path.line_to(point(px(x1) + origin.x, px(y1) + origin.y));
path.line_to(point(px(x2) + origin.x, px(y2) + origin.y));
window.paint_path(path, color);
let mut j = 0usize;
while j < batch.len() {
match batch[j] {
spec::draw_op::TRI => {
let color = abgr(batch[j + 4]);
let (x0, y0) = decode_xy(batch[j + 1]);
let (x1, y1) = decode_xy(batch[j + 2]);
let (x2, y2) = decode_xy(batch[j + 3]);
let mut path = Path::new(point(px(x0) + origin.x, px(y0) + origin.y));
path.line_to(point(px(x1) + origin.x, px(y1) + origin.y));
path.line_to(point(px(x2) + origin.x, px(y2) + origin.y));
window.paint_path(path, color);
j += 7;
}
spec::draw_op::POLY => {
let n = batch[j + 1] as usize;
let color = abgr(batch[j + 2]);
let (x0, y0) = decode_xy(batch[j + 3]);
let mut path = Path::new(point(px(x0) + origin.x, px(y0) + origin.y));
for k in 1..n {
let (x, y) = decode_xy(batch[j + 3 + k]);
path.line_to(point(px(x) + origin.x, px(y) + origin.y));
}
window.paint_path(path, color);
j += 3 + n;
}
_ => break,
}
}
return;
}
Expand All @@ -554,15 +584,17 @@ impl GpuiRenderer {
let mut key_words: Vec<u32> = batch.to_vec();
let mut j = 0usize;
while j < batch.len() {
if batch[j] == spec::draw_op::TEX_TRI {
let slot = batch[j + 1] & spec::TEX_SLOT_MASK;
if let Some((_, revision, _)) = ui.texture_at_versioned(slot) {
key_words.push(revision as u32);
key_words.push((revision >> 32) as u32);
match batch[j] {
spec::draw_op::TEX_TRI => {
let slot = batch[j + 1] & spec::TEX_SLOT_MASK;
if let Some((_, revision, _)) = ui.texture_at_versioned(slot) {
key_words.push(revision as u32);
key_words.push((revision >> 32) as u32);
}
j += 12;
}
j += 12;
} else {
j += 7;
spec::draw_op::POLY => j += 3 + batch[j + 1] as usize,
_ => j += 7,
}
}
let hash = fnv64(&key_words);
Expand All @@ -581,23 +613,39 @@ impl GpuiRenderer {
let (mut min_x, mut min_y, mut max_x, mut max_y) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
let mut j = 0usize;
while j < batch.len() {
let idxs: &[usize] = if batch[j] == spec::draw_op::TEX_TRI {
&[j + 2, j + 5, j + 8]
} else {
&[j + 1, j + 2, j + 3]
};
for &k in idxs {
let (x, y) = decode_xy(batch[k]);
min_x = min_x.min(x as i32);
min_y = min_y.min(y as i32);
max_x = max_x.max(x.ceil() as i32);
max_y = max_y.max(y.ceil() as i32);
match batch[j] {
spec::draw_op::TEX_TRI => {
for &k in &[j + 2, j + 5, j + 8] {
let (x, y) = decode_xy(batch[k]);
min_x = min_x.min(x as i32);
min_y = min_y.min(y as i32);
max_x = max_x.max(x.ceil() as i32);
max_y = max_y.max(y.ceil() as i32);
}
j += 12;
}
spec::draw_op::POLY => {
let n = batch[j + 1] as usize;
for k in 0..n {
let (x, y) = decode_xy(batch[j + 3 + k]);
min_x = min_x.min(x as i32);
min_y = min_y.min(y as i32);
max_x = max_x.max(x.ceil() as i32);
max_y = max_y.max(y.ceil() as i32);
}
j += 3 + n;
}
_ => {
for &k in &[j + 1, j + 2, j + 3] {
let (x, y) = decode_xy(batch[k]);
min_x = min_x.min(x as i32);
min_y = min_y.min(y as i32);
max_x = max_x.max(x.ceil() as i32);
max_y = max_y.max(y.ceil() as i32);
}
j += 7;
}
}
j += if batch[j] == spec::draw_op::TEX_TRI {
12
} else {
7
};
}
if min_x >= max_x || min_y >= max_y {
return;
Expand Down
26 changes: 26 additions & 0 deletions engine/core/src/damage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,13 @@ impl<'a> DamageDecoder<'a> {
spec::draw_op::SCISSOR_POP => 1,
spec::draw_op::TRI => 7,
spec::draw_op::TEX_TRI => 12,
spec::draw_op::POLY => {
let n = self.words.get(start + 1).copied().ok_or(())? as usize;
if !(3..=8).contains(&n) {
return Err(());
}
3usize.checked_add(n).ok_or(())?
}
spec::draw_op::TEXT_RUN => {
// 8 header words + ceil(byteLen/4) packed UTF-8 words.
let bytes = *self.words.get(start + 7).ok_or(())? as usize;
Expand Down Expand Up @@ -460,6 +467,7 @@ impl<'a> DamageDecoder<'a> {
}
spec::draw_op::TRI => triangle_bounds([words[1], words[2], words[3]], self.clip),
spec::draw_op::TEX_TRI => triangle_bounds([words[2], words[5], words[8]], self.clip),
spec::draw_op::POLY => polygon_bounds(&words[3..], self.clip),
// Native-text runs carry no glyph geometry the tracker can
// measure; the core keeps every partially-clipped run inside a
// scissor, so the current clip is a sound (conservative) bound.
Expand Down Expand Up @@ -561,6 +569,24 @@ fn triangle_bounds(vertices: [u32; 3], clip: DamageRect) -> DamageRect {
.intersect(clip)
}

fn polygon_bounds(vertices: &[u32], clip: DamageRect) -> DamageRect {
if vertices.is_empty() {
return DamageRect::empty();
}
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
for &word in vertices {
let (x, y) = xy(word);
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
DamageRect::new(min_x, min_y, max_x, max_y).intersect(clip)
}

#[inline]
fn xy(word: u32) -> (i32, i32) {
(
Expand Down
Loading