Files
MrLowPr--overdrive/ps5-react/patches/embeddedReact-image-radius.patch
2026-10-14 04:20:24 +02:00

321 lines
15 KiB
Diff

Embedded React: clip an Image to its borderRadius.
Modified from Embedded React (Apache-2.0, Copyright 2026 Cory Lamming) at the
revision pinned in dependencies.lock.json, after the patches listed before this
one there. Changed files: engine/scene/er_node_internal.h,
engine/scene/compositor.c, engine/rendering/image_scaler.c and
engine/tests/rendering/test_image.c. Proposed upstream as pull request #323.
Only backgrounds and gradients followed a node's corner radius, so an Image
with borderRadius (a card's cover) kept square corners. An Image node now keeps
its per-corner radii, and while one is set the bitmap is clipped to the same
rounded silhouette a background would fill: rows clear of the corner arcs blit
as one rectangle, rows the arcs cut blit their covered span and fade the
anti-aliased fringe as the gradient mask does. A square image takes the
unmasked paths unchanged. An overflow: hidden rounded parent still clips its
children to its rectangle only.
diff --git a/engine/rendering/image_scaler.c b/engine/rendering/image_scaler.c
index 2613534..51257fd 100644
--- a/engine/rendering/image_scaler.c
+++ b/engine/rendering/image_scaler.c
@@ -18,6 +18,7 @@
#include "er_limits.h"
#include "image_registry.h"
#include "renderer_internal.h"
+#include "rrect.h"
#include <math.h>
#include <stddef.h>
@@ -30,6 +31,18 @@
* blits parallel-safe (see the multi-core render fork in compositor.c). */
static uint32_t s_row_buf_pool[ERUI_RENDER_WORKERS][ERUI_MAX_IMG_ROW_PIXELS];
+/**
+ * @brief A rounded-rect silhouette the image is clipped to: the destination box and its clamped radii.
+ */
+typedef struct
+{
+ int x, y, w, h;
+ int r_tl, r_tr, r_br, r_bl;
+} ImageMask;
+
+/** The calling worker's silhouette while er_image_render() draws a rounded image; NULL otherwise. */
+static const ImageMask* s_mask[ERUI_RENDER_WORKERS];
+
/** @brief The calling worker's image row buffer. */
static inline uint32_t* irow(void)
{
@@ -193,6 +206,97 @@ static bool visible_part(int x, int y, int w, int h, int* x0, int* y0, int* x1,
return *x0 < *x1 && *y0 < *y1;
}
+/**
+ * @brief Blits one row of image pixels clipped to the rounded silhouette, as a rounded background is.
+ *
+ * The covered span blits as it is; the anti-aliased fringe pixels beside it are faded by the arc's
+ * coverage (the walk er_rrect_fill_corners and the gradient mask make), so an image corner has the
+ * shape a background with the same radius would have.
+ *
+ * @param[in] m Silhouette.
+ * @param[in] row Premultiplied pixels; row[i] lands on column dst_x + i.
+ * @param[in] dst_x Destination column of row[0].
+ * @param[in] y Destination row.
+ * @param[in] n Pixels in the row.
+ * @param[in] opaque The pixels are all opaque: the span replaces the destination.
+ */
+static void mask_row(const ImageMask* m, const uint32_t* row, int dst_x, int y, int n, bool opaque)
+{
+ ERRRectRow rr;
+ er_rrect_row(m->w, m->h, m->r_tl, m->r_tr, m->r_br, m->r_bl, y - m->y, &rr);
+ const int s0 = (m->x + rr.x0 > dst_x) ? m->x + rr.x0 : dst_x;
+ const int s1 = (m->x + rr.x1 < dst_x + n) ? m->x + rr.x1 : dst_x + n;
+ if (s1 > s0)
+ {
+ if (opaque)
+ er_blit_copy(&row[s0 - dst_x], (s1 - s0) * (int)sizeof(uint32_t), s0, y, s1 - s0, 1);
+ else
+ er_blit_blend(&row[s0 - dst_x], (s1 - s0) * (int)sizeof(uint32_t), 255, s0, y, s1 - s0, 1);
+ }
+#if ERUI_BORDER_AA
+ for (int k = 0, kmax = er_rrect_fringe_max(rr.l_r); k < kmax; k++)
+ {
+ const float cov = er_rrect_fringe_cov(rr.l_r, rr.l_dx, rr.l_dy, k);
+ if (cov <= 0.0f)
+ break;
+ const int ax = m->x + rr.x0 - 1 - k;
+ if (cov < 1.0f && ax >= dst_x && ax < dst_x + n)
+ {
+ const uint32_t p = er_px_scale_premul(row[ax - dst_x], (uint32_t)(cov * 255.0f + 0.5f));
+ er_blit_blend(&p, (int)sizeof(uint32_t), 255, ax, y, 1, 1);
+ }
+ }
+ for (int k = 0, kmax = er_rrect_fringe_max(rr.r_r); k < kmax; k++)
+ {
+ const float cov = er_rrect_fringe_cov(rr.r_r, rr.r_dx, rr.r_dy, k);
+ if (cov <= 0.0f)
+ break;
+ const int ax = m->x + rr.x1 + k;
+ if (cov < 1.0f && ax >= dst_x && ax < dst_x + n && ax < m->x + m->w)
+ {
+ const uint32_t p = er_px_scale_premul(row[ax - dst_x], (uint32_t)(cov * 255.0f + 0.5f));
+ er_blit_blend(&p, (int)sizeof(uint32_t), 255, ax, y, 1, 1);
+ }
+ }
+#endif
+}
+
+/**
+ * @brief Blits rows of image pixels clipped to the rounded silhouette: the rows clear of the corner
+ * arcs as one rectangle, the rows the arcs cut into one by one.
+ *
+ * @param[in] m Silhouette.
+ * @param[in] rows First row's pixels; rows[i] lands on column dst_x + i.
+ * @param[in] stride Row stride in pixels.
+ * @param[in] dst_x Destination column of each row's first pixel.
+ * @param[in] dst_y Destination row of the first row.
+ * @param[in] n Pixels per row.
+ * @param[in] count Rows.
+ * @param[in] opaque The pixels are all opaque.
+ */
+static void mask_rows(const ImageMask* m, const uint32_t* rows, int stride, int dst_x, int dst_y, int n, int count,
+ bool opaque)
+{
+ const int top = m->y + (m->r_tl > m->r_tr ? m->r_tl : m->r_tr);
+ const int bottom = m->y + m->h - (m->r_bl > m->r_br ? m->r_bl : m->r_br);
+ for (int i = 0; i < count;)
+ {
+ const int y = dst_y + i;
+ if (y < top || y >= bottom)
+ {
+ mask_row(m, rows + (size_t)i * (size_t)stride, dst_x, y, n, opaque);
+ i++;
+ continue;
+ }
+ const int run = ((bottom - y) < (count - i)) ? (bottom - y) : (count - i);
+ if (opaque)
+ er_blit_copy(rows + (size_t)i * (size_t)stride, stride * (int)sizeof(uint32_t), dst_x, y, n, run);
+ else
+ er_blit_blend(rows + (size_t)i * (size_t)stride, stride * (int)sizeof(uint32_t), 255, dst_x, y, n, run);
+ i += run;
+ }
+}
+
/**
* @brief Renders a source crop of an image to a destination rectangle.
*
@@ -247,6 +351,15 @@ static void render_region(const ImageEntry* img,
* backend-side); otherwise it degrades to the classic er_blit_copy. */
const int img_stride = img->w * (int)sizeof(uint32_t);
const uint32_t* rows = (const uint32_t*)img->buf + (size_t)src_y * (size_t)img->w + (size_t)src_x;
+ const ImageMask* const m = s_mask[er_render_worker_id()];
+ if (m)
+ {
+ int vx0, vy0, vx1, vy1;
+ if (visible_part(dst_x, dst_y, dst_w, dst_h, &vx0, &vy0, &vx1, &vy1))
+ mask_rows(m, rows + (size_t)vy0 * (size_t)img->w + (size_t)vx0, img->w, dst_x + vx0, dst_y + vy0,
+ vx1 - vx0, vy1 - vy0, img->opaque);
+ return;
+ }
if (img->opaque)
er_blit_copy_fmt(rows, img_stride, ER_IMG_ARGB8888, dst_x, dst_y, dst_w, dst_h);
else
@@ -260,7 +373,8 @@ static void render_region(const ImageEntry* img,
* that is ~1M conversions plus one backend call per scanline. */
const int stride565 = img->w * (int)sizeof(uint16_t);
const uint16_t* rows565 = (const uint16_t*)img->buf + (size_t)src_y * (size_t)img->w + (size_t)src_x;
- if (er_blit_copy_fmt(rows565, stride565, ER_IMG_RGB565, dst_x, dst_y, dst_w, dst_h))
+ const ImageMask* const m565 = s_mask[er_render_worker_id()];
+ if (!m565 && er_blit_copy_fmt(rows565, stride565, ER_IMG_RGB565, dst_x, dst_y, dst_w, dst_h))
return;
/* CPU fallback (no copy_rect_fmt backend, scratch capture, or inherited alpha): expand each
@@ -278,7 +392,10 @@ static void render_region(const ImageEntry* img,
const int cw = (vx1 - cx) < ERUI_MAX_IMG_ROW_PIXELS ? (vx1 - cx) : ERUI_MAX_IMG_ROW_PIXELS;
for (int dx = 0; dx < cw; dx++)
irow()[dx] = rgb565_to_argb(srow[cx + dx]);
- er_blit_copy(irow(), cw * (int)sizeof(uint32_t), dst_x + cx, dst_y + dy, cw, 1);
+ if (m565)
+ mask_row(m565, irow(), dst_x + cx, dst_y + dy, cw, true);
+ else
+ er_blit_copy(irow(), cw * (int)sizeof(uint32_t), dst_x + cx, dst_y + dy, cw, 1);
}
}
return;
@@ -309,7 +426,10 @@ static void render_region(const ImageEntry* img,
for (int i = 0; i < vis_w; i++)
row[i] = apply_tint(row[i], tr, tg, tb);
/* Tint preserves alpha, and integer weights keep opaque samples exactly opaque. */
- if (img->opaque)
+ const ImageMask* const m = s_mask[er_render_worker_id()];
+ if (m)
+ mask_row(m, row, dst_x + vx0, dst_y + dy, vis_w, img->opaque);
+ else if (img->opaque)
er_blit_copy(row, vis_w * (int)sizeof(uint32_t), dst_x + vx0, dst_y + dy, vis_w, 1);
else
er_blit_blend(row, vis_w * (int)sizeof(uint32_t), 255, dst_x + vx0, dst_y + dy, vis_w, 1);
@@ -355,6 +475,12 @@ void er_image_render(const ERImageProps* props, int x, int y, int w, int h)
const uint8_t tg = has_tint ? (uint8_t)((tint >> 8) & 0xFFu) : 0u;
const uint8_t tb = has_tint ? (uint8_t)(tint & 0xFFu) : 0u;
+ /* A rounded image is clipped to its box's silhouette; a square one takes the unmasked paths. */
+ ImageMask mask = {x, y, w, h, props->radius_tl, props->radius_tr, props->radius_br, props->radius_bl};
+ er_rrect_clamp_radii(w, h, &mask.r_tl, &mask.r_tr, &mask.r_br, &mask.r_bl);
+ const int worker = er_render_worker_id();
+ s_mask[worker] = (mask.r_tl > 0 || mask.r_tr > 0 || mask.r_br > 0 || mask.r_bl > 0) ? &mask : NULL;
+
switch ((ERResizeMode)props->resize_mode)
{
default:
@@ -438,4 +564,5 @@ void er_image_render(const ERImageProps* props, int x, int y, int w, int h)
break;
}
}
+ s_mask[worker] = NULL;
}
diff --git a/engine/scene/compositor.c b/engine/scene/compositor.c
index e5ad8a0..0bcf041 100644
--- a/engine/scene/compositor.c
+++ b/engine/scene/compositor.c
@@ -3530,6 +3530,15 @@ void er_node_set_props(ERNode* node, const ERProps* props)
node->props.image.image_name[ER_IMAGE_NAME_MAX] = '\0';
node->props.image.resize_mode = props->resize_mode;
node->props.image.tint_color = props->tint_color;
+ /* Per-corner radius, falling back to the uniform one, as a View's background resolves it. */
+ node->props.image.radius_tl =
+ props->border_top_left_radius > 0 ? props->border_top_left_radius : props->border_radius;
+ node->props.image.radius_tr =
+ props->border_top_right_radius > 0 ? props->border_top_right_radius : props->border_radius;
+ node->props.image.radius_br =
+ props->border_bottom_right_radius > 0 ? props->border_bottom_right_radius : props->border_radius;
+ node->props.image.radius_bl =
+ props->border_bottom_left_radius > 0 ? props->border_bottom_left_radius : props->border_radius;
break;
case ER_NODE_ACTIVITY_INDICATOR:
{
diff --git a/engine/scene/er_node_internal.h b/engine/scene/er_node_internal.h
index 75886f3..9f9062c 100644
--- a/engine/scene/er_node_internal.h
+++ b/engine/scene/er_node_internal.h
@@ -258,6 +258,10 @@ typedef struct
char image_name[ER_IMAGE_NAME_MAX + 1];
uint8_t resize_mode; /**< ERResizeMode */
uint32_t tint_color; /**< Straight-alpha ARGB8888 tint; 0 = no tint. */
+ int16_t radius_tl; /**< Corner radii the bitmap is clipped to (borderRadius, per corner); 0 = square. */
+ int16_t radius_tr;
+ int16_t radius_br;
+ int16_t radius_bl;
} ERImageProps;
/**
diff --git a/engine/tests/rendering/test_image.c b/engine/tests/rendering/test_image.c
index 70e0790..85bd492 100644
--- a/engine/tests/rendering/test_image.c
+++ b/engine/tests/rendering/test_image.c
@@ -982,5 +982,65 @@ int main(void)
}
}
+ /* -----------------------------------------------------------------------
+ * borderRadius clips the bitmap to the rounded silhouette, at 1:1 and scaled:
+ * the corner pixel stays empty, the edges and centre are the image, and the
+ * anti-aliased fringe is partly covered. Radius 0 leaves the corner painted.
+ * ---------------------------------------------------------------------- */
+ {
+ static uint32_t img16[16 * 16];
+ for (int i = 0; i < 16 * 16; i++)
+ img16[i] = 0xFF00FFFF;
+ er_image_load("test16", img16, 16, 16);
+ er_image_load("test16of4", img16, 4, 4); /* the same cyan, scaled up 4x */
+ const char* const names[] = {"test16", "test16of4"};
+ for (int k = 0; k < 2; k++)
+ {
+ for (int radius = 0; radius <= 6; radius += 6)
+ {
+ reset(&tc);
+ er_reset();
+ ERNode* root = er_node_create(ER_NODE_VIEW);
+ ERProps rp = props_default();
+ rp.width = FB_W;
+ rp.height = FB_H;
+ er_node_set_props(root, &rp);
+ ERNode* img_node = er_node_create(ER_NODE_IMAGE);
+ ERProps ip = props_default();
+ ip.width = 16;
+ ip.height = 16;
+ ip.border_radius = (int16_t)radius;
+ strncpy(ip.image_name, names[k], ER_IMAGE_NAME_MAX);
+ ip.resize_mode = ER_RESIZE_COVER;
+ er_node_set_props(img_node, &ip);
+ er_tree_append_child(root, img_node);
+ er_tree_set_root(root);
+ er_commit();
+
+ if (px(&tc, 8, 8) != 0xFF00FFFF || px(&tc, 8, 0) != 0xFF00FFFF || px(&tc, 0, 8) != 0xFF00FFFF
+ || px(&tc, 15, 8) != 0xFF00FFFF || px(&tc, 8, 15) != 0xFF00FFFF)
+ return fail("rounded image: centre or edge midpoints are not the image");
+ if (radius == 0 && px(&tc, 0, 0) != 0xFF00FFFF)
+ return fail("square image: corner not painted");
+ if (radius > 0 && (px(&tc, 0, 0) != 0 || px(&tc, 15, 0) != 0 || px(&tc, 0, 15) != 0 || px(&tc, 15, 15) != 0))
+ return fail(k ? "rounded image: scaled corner painted" : "rounded image: 1:1 corner painted");
+ if (radius > 0)
+ {
+ /* Some pixel on the arc is partly covered: the fringe is anti-aliased. */
+ bool partial = false;
+ for (int y = 0; y < radius && !partial; y++)
+ for (int x = 0; x < radius && !partial; x++)
+ partial = px(&tc, x, y) != 0 && px(&tc, x, y) != 0xFF00FFFF;
+ if (!partial)
+ return fail("rounded image: no anti-aliased fringe pixel at the corner");
+ }
+
+ er_tree_remove_child(root, img_node);
+ er_node_destroy(img_node);
+ er_node_destroy(root);
+ }
+ }
+ }
+
return EXIT_SUCCESS;
}