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

436 lines
17 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Embedded React: move a translated node's subtree with it.
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/compositor.c,
engine/tests/rendering/CMakeLists.txt and the new
engine/tests/rendering/test_anim_translate_offscreen.c. Proposed upstream as
pull request #311.
The translate-only fast path offset a node's own paint by its translate but
handed its children the untranslated offset, so they kept painting at their
layout boxes. A header slid up past the screen's top edge left its items
behind, frozen on screen. The fast path now passes the translate down with the
scroll offset, and the damage passes measure descendants the same way
(node_ancestor_scroll), so the trail they vacate is repainted. Translates that
go through the transform capture already moved their subtree and are unchanged.
diff --git a/engine/scene/compositor.c b/engine/scene/compositor.c
index dbf814b..b57968d 100644
--- a/engine/scene/compositor.c
+++ b/engine/scene/compositor.c
@@ -659,13 +659,32 @@ static void add_damage(ERDamageSet* s, int x, int y, int w, int h, int rx0, int
}
/**
- * @brief Sums the scroll offsets of every ScrollView / FlatList above a node.
+ * @brief Whether render_tree paints a node through the translate-only fast path: offset by its
+ * translate, with its whole subtree, instead of through the transform capture.
+ *
+ * @param[in] n Node to inspect.
+ *
+ * @return true when the node's translate moves its subtree as a plain pixel offset.
+ */
+static bool node_translates_subtree(const ERNode* n)
+{
+#if ERUI_TRANSFORMS_FULL
+ return n->has_transform && !er_node_has_complex_transform(n);
+#else
+ return n->has_transform;
+#endif
+}
+
+/**
+ * @brief Sums the scroll offsets of every ScrollView / FlatList above a node, less the translate of
+ * every ancestor painted through the translate-only fast path.
*
* render_tree carries this down the walk as its translation; the flat per-node damage passes have no
- * parent context, so they re-derive it here.
+ * parent context, so they re-derive it here. A translated ancestor moves its descendants with it, so
+ * it counts like a scroll in the opposite direction.
*
- * @param[in] n Node to measure from (its own scroll offset is NOT included).
- * @param[out] sx,sy Receive the accumulated ancestor scroll.
+ * @param[in] n Node to measure from (its own scroll offset and translate are NOT included).
+ * @param[out] sx,sy Receive the accumulated ancestor offset.
*/
static void node_ancestor_scroll(const ERNode* n, int* sx, int* sy)
{
@@ -679,6 +698,11 @@ static void node_ancestor_scroll(const ERNode* n, int* sx, int* sy)
*sx += (int)a->scroll_offset_x;
*sy += (int)a->scroll_offset_y;
}
+ if (node_translates_subtree(a))
+ {
+ *sx -= (int)a->tp_translate_x;
+ *sy -= (int)a->tp_translate_y;
+ }
a = er_get_node(a->parent_tag);
}
}
@@ -2308,9 +2332,12 @@ static void render_tree(ERNode* n, bool parent_dirty, bool occluded, int transla
else
#endif
{
- /* Translate-only fast path: shift the render position by the prop offsets. */
+ /* Translate-only fast path: shift the render position by the prop offsets, and the
+ * subtree with it (node_ancestor_scroll() measures descendants the same way). */
px += (int)n->tp_translate_x;
py += (int)n->tp_translate_y;
+ translate_x -= (int)n->tp_translate_x;
+ translate_y -= (int)n->tp_translate_y;
}
}
diff --git a/engine/tests/rendering/CMakeLists.txt b/engine/tests/rendering/CMakeLists.txt
index 382fc98..799bacb 100644
--- a/engine/tests/rendering/CMakeLists.txt
+++ b/engine/tests/rendering/CMakeLists.txt
@@ -26,6 +26,11 @@ target_link_libraries(test_anim_transform_damage PRIVATE embedded-react)
target_include_directories(test_anim_transform_damage PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../core)
add_test(NAME rendering_anim_transform_damage COMMAND test_anim_transform_damage)
+add_executable(test_anim_translate_offscreen test_anim_translate_offscreen.c)
+target_link_libraries(test_anim_translate_offscreen PRIVATE embedded-react)
+target_include_directories(test_anim_translate_offscreen PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../core)
+add_test(NAME rendering_anim_translate_offscreen COMMAND test_anim_translate_offscreen)
+
add_executable(test_parallel test_parallel.c)
target_link_libraries(test_parallel PRIVATE embedded-react Threads::Threads)
# test_parallel needs engine/core for renderer_internal.h (er_parallel_for / worker contexts).
diff --git a/engine/tests/rendering/test_anim_translate_offscreen.c b/engine/tests/rendering/test_anim_translate_offscreen.c
new file mode 100644
index 0000000..7f6bf4a
--- /dev/null
+++ b/engine/tests/rendering/test_anim_translate_offscreen.c
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2026 Cory Lamming
+ * Copyright 2026 PS5 React contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * A translate moves a node's whole subtree. A header slid up past the screen's top edge by a
+ * native-driver translate must take its children with it, and every frame of the slide, the settled
+ * ones included, must match a full repaint.
+ *
+ * The bug this guards: the translate-only fast path offset the node's own paint but not its
+ * descendants', which kept painting at their untranslated boxes, so the header's items stayed on
+ * screen after it left.
+ */
+
+#include "er_scene.h"
+#include "native_renderer.h"
+#include "renderer_internal.h"
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*----------------------------------------------------------------------------------------------------------------------
+ - Constants
+ ---------------------------------------------------------------------------------------------------------------------*/
+
+#define FB_W 2560
+#define FB_H 400
+
+#define ANIM_MS 480 /* animation duration */
+#define FRAME_MS 16 /* per-tick advance */
+#define FRAMES 40 /* ticks driven (runs past the end so the settled frames are checked too) */
+#define ITEM_COLOR 0xFF3DDC84U
+
+/*----------------------------------------------------------------------------------------------------------------------
+ - Types: Private
+ ---------------------------------------------------------------------------------------------------------------------*/
+
+/**
+ * @brief Framebuffer the backend stubs rasterise into.
+ */
+typedef struct
+{
+ uint32_t* fb; /**< Flat ARGB8888 framebuffer. */
+ int fb_w; /**< Width in pixels. */
+ int fb_h; /**< Height in pixels. */
+} TestCtx;
+
+/*----------------------------------------------------------------------------------------------------------------------
+ - Functions: Private — backend stubs
+ ---------------------------------------------------------------------------------------------------------------------*/
+
+/** @brief Rounds a 0–65025 product back to 0–255 the way the engine's blenders do. */
+static uint32_t div255(uint32_t v)
+{
+ return (v + 127U) / 255U;
+}
+
+/**
+ * @brief Backend fill_rect: source-over composites a straight-alpha colour into the framebuffer.
+ *
+ * @param[in] argb Straight-alpha ARGB8888 fill colour.
+ * @param[in] x Left edge.
+ * @param[in] y Top edge.
+ * @param[in] w Width in pixels.
+ * @param[in] h Height in pixels.
+ * @param[in] ctx Pointer to TestCtx.
+ */
+static void fill_cb(uint32_t argb, int x, int y, int w, int h, void* ctx)
+{
+ TestCtx* t = ctx;
+ const uint32_t a = (argb >> 24) & 0xFFU;
+ if (a == 0U || w <= 0 || h <= 0)
+ return;
+ const uint32_t inv = 255U - a;
+ const uint32_t sr = (argb >> 16) & 0xFFU, sg = (argb >> 8) & 0xFFU, sb = argb & 0xFFU;
+
+ for (int row = y; row < y + h; row++)
+ {
+ if (row < 0 || row >= t->fb_h)
+ continue;
+ for (int col = x; col < x + w; col++)
+ {
+ if (col < 0 || col >= t->fb_w)
+ continue;
+ uint32_t* d = &t->fb[row * t->fb_w + col];
+ const uint32_t dr = (*d >> 16) & 0xFFU, dg = (*d >> 8) & 0xFFU, db = *d & 0xFFU;
+ *d = 0xFF000000U | ((div255(sr * a) + div255(dr * inv)) << 16) | ((div255(sg * a) + div255(dg * inv)) << 8)
+ | (div255(sb * a) + div255(db * inv));
+ }
+ }
+}
+
+/**
+ * @brief Backend copy_rect: source-over composites a premultiplied buffer into the framebuffer.
+ *
+ * Fully transparent source pixels are skipped and the rest blended, matching the engine's own
+ * scratch copy — an unconditional overwrite would erase the background under antialiased edges and
+ * make the comparison depend on repaint history rather than on scene state.
+ *
+ * @param[in] src Source pixel buffer (premultiplied ARGB8888).
+ * @param[in] stride Source row stride in bytes.
+ * @param[in] x Destination left edge.
+ * @param[in] y Destination top edge.
+ * @param[in] w Width in pixels.
+ * @param[in] h Height in pixels.
+ * @param[in] ctx Pointer to TestCtx.
+ */
+static void copy_cb(const void* src, int stride, int x, int y, int w, int h, void* ctx)
+{
+ TestCtx* t = ctx;
+ for (int row = 0; row < h; row++)
+ {
+ const uint32_t* s = (const uint32_t*)((const uint8_t*)src + (size_t)row * (size_t)stride);
+ for (int col = 0; col < w; col++)
+ {
+ const int dx = x + col, dy = y + row;
+ if (dx < 0 || dx >= t->fb_w || dy < 0 || dy >= t->fb_h)
+ continue;
+ const uint32_t sp = s[col];
+ const uint32_t sa = (sp >> 24) & 0xFFU;
+ if (sa == 0U)
+ continue;
+ uint32_t* d = &t->fb[dy * t->fb_w + dx];
+ if (sa == 0xFFU)
+ {
+ *d = sp;
+ continue;
+ }
+ const uint32_t inv = 255U - sa;
+ const uint32_t dr = (*d >> 16) & 0xFFU, dg = (*d >> 8) & 0xFFU, db = *d & 0xFFU;
+ *d = 0xFF000000U | ((((sp >> 16) & 0xFFU) + div255(dr * inv)) << 16)
+ | ((((sp >> 8) & 0xFFU) + div255(dg * inv)) << 8) | ((sp & 0xFFU) + div255(db * inv));
+ }
+ }
+}
+
+/**
+ * @brief Backend blend_rect: source-over composites premultiplied pixels scaled by a global alpha.
+ *
+ * @param[in] src Source pixel buffer (premultiplied ARGB8888).
+ * @param[in] stride Source row stride in bytes.
+ * @param[in] alpha Global alpha scale 0–255.
+ * @param[in] x Destination left edge.
+ * @param[in] y Destination top edge.
+ * @param[in] w Width in pixels.
+ * @param[in] h Height in pixels.
+ * @param[in] ctx Pointer to TestCtx.
+ */
+static void blend_cb(const void* src, int stride, uint8_t alpha, int x, int y, int w, int h, void* ctx)
+{
+ TestCtx* t = ctx;
+ for (int row = 0; row < h; row++)
+ {
+ const uint32_t* s = (const uint32_t*)((const uint8_t*)src + (size_t)row * (size_t)stride);
+ for (int col = 0; col < w; col++)
+ {
+ const int dx = x + col, dy = y + row;
+ if (dx < 0 || dx >= t->fb_w || dy < 0 || dy >= t->fb_h)
+ continue;
+ const uint32_t sp = s[col];
+ const uint32_t sa = div255(((sp >> 24) & 0xFFU) * (uint32_t)alpha);
+ if (sa == 0U)
+ continue;
+ const uint32_t inv = 255U - sa;
+ /* Source is premultiplied: scale its colour by the same global alpha, not by sa. */
+ const uint32_t sr = div255(((sp >> 16) & 0xFFU) * (uint32_t)alpha);
+ const uint32_t sg = div255(((sp >> 8) & 0xFFU) * (uint32_t)alpha);
+ const uint32_t sb = div255((sp & 0xFFU) * (uint32_t)alpha);
+ uint32_t* d = &t->fb[dy * t->fb_w + dx];
+ const uint32_t dr = (*d >> 16) & 0xFFU, dg = (*d >> 8) & 0xFFU, db = *d & 0xFFU;
+ *d = 0xFF000000U | ((sr + div255(dr * inv)) << 16) | ((sg + div255(dg * inv)) << 8)
+ | (sb + div255(db * inv));
+ }
+ }
+}
+
+/*----------------------------------------------------------------------------------------------------------------------
+ - Functions: Private — helpers
+ ---------------------------------------------------------------------------------------------------------------------*/
+
+/**
+ * @brief Prints a failure message.
+ *
+ * @param[in] msg Message describing the failed assertion.
+ *
+ * @return EXIT_FAILURE, so callers can `return fail(...)`.
+ */
+static int fail(const char* msg)
+{
+ fprintf(stderr, "FAIL: %s\n", msg);
+ return EXIT_FAILURE;
+}
+
+/**
+ * @brief Returns an ERProps with every int16_t layout field set to ER_LAYOUT_AUTO.
+ *
+ * @return Initialised ERProps with opacity 255.
+ */
+static ERProps props_default(void)
+{
+ ERProps p = {0};
+ p.left = p.top = p.right = p.bottom = ER_LAYOUT_AUTO;
+ p.width = p.height = ER_LAYOUT_AUTO;
+ p.min_width = p.max_width = ER_LAYOUT_AUTO;
+ p.min_height = p.max_height = ER_LAYOUT_AUTO;
+ p.padding = p.padding_left = p.padding_top = ER_LAYOUT_AUTO;
+ p.padding_right = p.padding_bottom = ER_LAYOUT_AUTO;
+ p.margin = p.margin_left = p.margin_top = ER_LAYOUT_AUTO;
+ p.margin_right = p.margin_bottom = ER_LAYOUT_AUTO;
+ p.gap = p.row_gap = p.column_gap = ER_LAYOUT_AUTO;
+ p.flex_basis = ER_LAYOUT_AUTO;
+ p.opacity = 255U;
+ return p;
+}
+
+/**
+ * @brief Lifts a header holding one item to translateY `to`, checking every frame and where the item ends.
+ *
+ * @param[in,out] t TestCtx owning the framebuffer.
+ * @param[in] to Final translateY, at least 64 px up.
+ * @param[in] translucent Paint the header 90% black instead of opaque, like an overlay bar.
+ *
+ * @return EXIT_SUCCESS on pass, EXIT_FAILURE on the first failed check.
+ */
+static int run_lift(TestCtx* t, float to, bool translucent)
+{
+ static uint32_t incremental[FB_W * FB_H];
+
+ er_reset();
+ memset(t->fb, 0, sizeof(uint32_t) * FB_W * FB_H);
+
+ ERNode* root = er_node_create(ER_NODE_VIEW);
+ ERProps rp = props_default();
+ rp.width = FB_W;
+ rp.height = FB_H;
+ rp.background_color = 0xFF101010U;
+ er_node_set_props(root, &rp);
+
+ ERNode* header = er_node_create(ER_NODE_VIEW);
+ ERProps hp = props_default();
+ hp.position = ER_POS_ABSOLUTE;
+ hp.left = 0;
+ hp.top = 0;
+ hp.width = FB_W;
+ hp.height = 128;
+ hp.background_color = translucent ? 0xE6000000U : 0xFF4DA3FFU;
+ er_node_set_props(header, &hp);
+
+ ERNode* item = er_node_create(ER_NODE_VIEW);
+ ERProps ip = props_default();
+ ip.width = 200;
+ ip.height = 80;
+ ip.background_color = ITEM_COLOR;
+ er_node_set_props(item, &ip);
+
+ er_tree_append_child(header, item);
+ er_tree_append_child(root, header);
+ er_tree_set_root(root);
+ er_force_full_repaint();
+ er_commit();
+
+ ERAnimConfig cfg = {0};
+ cfg.type = ER_ANIM_TIMING;
+ cfg.duration_ms = ANIM_MS;
+ cfg.easing = ER_EASE_LINEAR;
+ ERAnimValueHandle lift = er_anim_value_create(0.0f);
+ er_anim_value_bind(lift, header, ER_PROP_TRANSLATE_Y);
+ er_anim_value_animate(lift, to, &cfg);
+
+ for (int f = 0; f < FRAMES; f++)
+ {
+ embedded_renderer_tick(FRAME_MS);
+ er_commit();
+ memcpy(incremental, t->fb, sizeof(incremental));
+
+ memset(t->fb, 0, sizeof(uint32_t) * FB_W * FB_H);
+ er_force_full_repaint();
+ er_commit();
+
+ if (memcmp(incremental, t->fb, sizeof(incremental)) != 0)
+ {
+ char msg[160];
+ snprintf(msg, sizeof(msg), "lift to %.0f (%s): frame %d differs from a full repaint", to,
+ translucent ? "translucent" : "opaque", f);
+ return fail(msg);
+ }
+ memcpy(t->fb, incremental, sizeof(incremental));
+ }
+ /* Settled: the item moved up with its header, by at least 64 px, so 20 px down it is gone. Comparing
+ * against a full repaint alone cannot see this: both drew the item at its untranslated box. */
+ if (t->fb[20 * FB_W + 10] == ITEM_COLOR)
+ {
+ char msg[160];
+ snprintf(msg, sizeof(msg), "lift to %.0f (%s): the item stayed where its header left it", to,
+ translucent ? "translucent" : "opaque");
+ return fail(msg);
+ }
+ return EXIT_SUCCESS;
+}
+
+int main(void)
+{
+ static uint32_t fb[FB_W * FB_H];
+ TestCtx tc = {fb, FB_W, FB_H};
+ EmbeddedRenderBackend be = {fill_cb, copy_cb, blend_cb, NULL, NULL, &tc};
+ embedded_renderer_set_backend(&be);
+
+ int result = EXIT_SUCCESS;
+ const float targets[] = {-64.0f, -128.0f, -400.0f};
+ for (int i = 0; i < 3; i++)
+ for (int translucent = 0; translucent < 2; translucent++)
+ if (run_lift(&tc, targets[i], translucent) != EXIT_SUCCESS)
+ result = EXIT_FAILURE;
+ return result;
+}