summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-15 05:14:54 +0900
committerAdam Malczewski <[email protected]>2026-06-15 05:14:54 +0900
commite41dbe376820ccbe120b9e27429cccd4345293d0 (patch)
treeb19d7b082e525e24817613874a6ea0b24a0be6e8
parented83e73787f4e0830791359b39d3bf18c84d8167 (diff)
downloadunbox-e41dbe376820ccbe120b9e27429cccd4345293d0.tar.gz
unbox-e41dbe376820ccbe120b9e27429cccd4345293d0.zip
spike(kernel): fix transform-incorrect click routing + live click-debug overlay
The bug: route_point mapped the cursor with transform-UNAWARE math — `(screen - GetAbsoluteOffset) / box`. screen coords are in transformed screen space; GetAbsoluteOffset is in untransformed layout space. They coincide only for an axis-aligned window, so plain --run tested fine, but on a --demo corner (perspective + rotateX/rotateY) they diverge and clicks landed in the wrong spot inside the client. --verify criterion 3 stayed green because it exercised a DIFFERENT, correct path (unproject_to_local) the live code never called. The fix: project the screen point onto the element's own plane FIRST via RmlUi's transform-aware Element::Project() (ray/plane intersect through the element's accumulated transform), then do the box->surface-local math in the matching space. Project() is a no-op when untransformed, so --run is unchanged. This is the live-path analogue of the pure-core inverse criterion 3 verifies. Debug tool (press D in --demo): a per-surface magenta crosshair drawn as a child of the hovered window, so RmlUi renders it THROUGH that window's own 3D transform — it sits exactly under the wlr cursor iff the mapping is correct, so any gap is the live click error, visible live (no screenshots). A #dbg readout prints screen -> elem-local -> surface-local (also to the diagnostic log). 'D' is consumed so it never types into the focused client. Confirmed accurate on the real CF-AX3 seat. --verify ALL PASS, kernel suite green, spike builds clean.
-rw-r--r--packages/kernel/src/spike/rml_compositing_spike_run.cpp153
1 files changed, 144 insertions, 9 deletions
diff --git a/packages/kernel/src/spike/rml_compositing_spike_run.cpp b/packages/kernel/src/spike/rml_compositing_spike_run.cpp
index 5897563..eb27123 100644
--- a/packages/kernel/src/spike/rml_compositing_spike_run.cpp
+++ b/packages/kernel/src/spike/rml_compositing_spike_run.cpp
@@ -299,6 +299,16 @@ struct Runner {
long bucket_frames0 = 0; // demo_frames at the bucket open (for frames=N)
bool bucket_has = false; // a sample has landed in this bucket yet
+ // --- click-accuracy debug overlay (toggle 'D') ---------------------------
+ // Off by default. When on, route_point places a per-surface crosshair marker
+ // at the mapped hit point (rendered through the surface's own transform, so it
+ // overlays the wlr cursor iff the mapping is correct) and writes a numeric
+ // readout (screen -> projected element-local -> surface-local) to #dbg + log.
+ bool debug_overlay = false;
+ Rml::Element* dbg_el = nullptr; // the #dbg numeric readout box
+ double last_cursor_x = 0.0; // last screen cursor pos, to re-place on toggle
+ double last_cursor_y = 0.0;
+
spike::GlBridge gl;
spike::PresentTarget present;
Rml::Context* ctx = nullptr;
@@ -426,12 +436,26 @@ body { margin: 0px; padding: 0px; perspective: 1100px; background: #05070d; }
font-family: "Noto Sans"; }
#hud p { display: block; margin: 8px 0px 0px 0px; }
#hud .big { font-size: 34px; color: #ffffff; }
+/* Click-accuracy debug overlay (toggle with 'D'). #dbg is the numeric readout;
+ each surface gets a .xhair marker child drawn THROUGH its own transform at the
+ computed hit point — if the mapping is correct it sits exactly under the wlr
+ cursor, so any gap is the live click error, visible directly (no screenshots). */
+#dbg { display: none; position: absolute; left: 8px; top: 8px;
+ width: 760px; padding: 10px;
+ background: #000000d0; border: 2px #ff2e9a;
+ font-size: 16px; color: #ff66cc; font-family: "Noto Sans"; }
+#dbg p { display: block; margin: 0px 0px 4px 0px; }
+.xhair { display: block; position: absolute; width: 18px; height: 18px;
+ border: 2px #ff2e9aff; background: #ff2e9a44; }
+.xhair .dot { display: block; position: absolute; left: 7px; top: 7px;
+ width: 4px; height: 4px; background: #ffffffff; }
</style>
</head>
<body>
<div id="wall"></div>
<div id="stage"></div>
<div id="hud"><p>compositor FPS</p><p class="big">--</p></div>
+<div id="dbg"><p>click-debug: press D to toggle</p></div>
</body>
</rml>)RML";
@@ -769,35 +793,121 @@ struct Routed {
double sx = 0, sy = 0; // surface-local pixels
};
+// Click-accuracy debug overlay: find-or-create the per-surface crosshair marker
+// as a child of the surface element `win`, so RmlUi draws it THROUGH win's own
+// transform — i.e. at the on-screen forward-projection of the mapped point.
+auto find_or_make_xhair(Runner& r, LiveSurface& s, Rml::Element* win) -> Rml::Element* {
+ const Rml::String xid = s.element_id + "_xhair";
+ Rml::Element* x = r.doc->GetElementById(xid);
+ if (x == nullptr) {
+ Rml::ElementPtr xp = r.doc->CreateElement("div");
+ xp->SetId(xid);
+ xp->SetClass("xhair", true);
+ Rml::ElementPtr dot = r.doc->CreateElement("div");
+ dot->SetClass("dot", true);
+ xp->AppendChild(std::move(dot));
+ x = win->AppendChild(std::move(xp));
+ }
+ return x;
+}
+
+// Place the crosshair at the mapped local point (win-local space) and update the
+// #dbg readout. The marker rides win's transform, so if route_point's inverse is
+// consistent with RmlUi's forward render it lands exactly under the wlr cursor;
+// any visible gap is the live click error. Also logged so numbers reach the file.
+void update_debug_marker(Runner& r, LiveSurface& s, Rml::Element* win, const Rml::Vector2f& doc_pt,
+ double sx, double sy, double screen_x, double screen_y) {
+ if (win == nullptr) {
+ return;
+ }
+ Rml::Element* x = find_or_make_xhair(r, s, win);
+ const Rml::Vector2f pad = win->GetAbsoluteOffset(Rml::BoxArea::Padding);
+ x->SetProperty("left", std::to_string(doc_pt.x - pad.x - 9.0) + "px");
+ x->SetProperty("top", std::to_string(doc_pt.y - pad.y - 9.0) + "px");
+ x->SetProperty("display", "block");
+ if (r.dbg_el != nullptr) {
+ char buf[320];
+ std::snprintf(buf, sizeof(buf),
+ "<p>DEBUG (D toggles) — marker should sit UNDER the cursor</p>"
+ "<p>surface '%s' tex %dx%d</p>"
+ "<p>screen (%.1f, %.1f) -&gt; elem-local (%.1f, %.1f)</p>"
+ "<p>surface-local (%.1f, %.1f)</p>",
+ s.element_id.c_str(), s.live.width, s.live.height, screen_x, screen_y, doc_pt.x,
+ doc_pt.y, sx, sy);
+ r.dbg_el->SetInnerRML(buf);
+ r.dbg_el->SetProperty("display", "block");
+ }
+ r.dirty = true;
+}
+
+// Hide every surface's crosshair + the readout (debug toggled off, or a miss).
+void hide_debug_markers(Runner& r) {
+ for (LiveSurface& s : r.surfaces) {
+ if (Rml::Element* x = r.doc->GetElementById(s.element_id + "_xhair")) {
+ x->SetProperty("display", "none");
+ }
+ }
+ if (r.dbg_el != nullptr) {
+ r.dbg_el->SetProperty("display", "none");
+ }
+ r.dirty = true;
+}
+
auto route_point(Runner& r, double screen_x, double screen_y) -> Routed {
+ r.last_cursor_x = screen_x;
+ r.last_cursor_y = screen_y;
r.ctx->ProcessMouseMove(static_cast<int>(screen_x), static_cast<int>(screen_y), 0);
Rml::Element* hover = r.ctx->GetHoverElement();
LiveSurface* s = surface_for_element(r, hover);
if (s == nullptr) {
+ if (r.debug_overlay && r.dbg_el != nullptr) {
+ char buf[160];
+ std::snprintf(buf, sizeof(buf),
+ "<p>DEBUG (D toggles)</p><p>no surface under (%.1f, %.1f)</p>", screen_x,
+ screen_y);
+ r.dbg_el->SetInnerRML(buf);
+ r.dbg_el->SetProperty("display", "block");
+ }
return {};
}
- // The <img> child carries the texture box; map the screen point into its
- // content box (RmlUi gives us the transform-resolved absolute offset) and
- // scale to the live texture's natural pixels = surface-local coords.
- Rml::Element* img = r.doc->GetElementById(s->element_id);
- if (img != nullptr && img->GetFirstChild() != nullptr) {
- img = img->GetFirstChild();
- }
+ // The surface element `win` carries the per-corner transform; its <img> child
+ // carries the texture box. Map the screen point into the img content box.
+ Rml::Element* win = r.doc->GetElementById(s->element_id);
+ Rml::Element* img = (win != nullptr && win->GetFirstChild() != nullptr) ? win->GetFirstChild()
+ : win;
if (img == nullptr) {
return {};
}
+ // THE FIX: project the screen point onto the element's OWN (possibly
+ // 3D-transformed) plane FIRST, using the element's accumulated transform.
+ // Element::Project() ray-casts the window point through the inverse transform
+ // onto the element's z=0 plane and returns it in the element's UNTRANSFORMED
+ // document space — the same space as GetAbsoluteOffset below. For an
+ // untransformed element it is a no-op (returns the point unchanged), so the
+ // plain --run path is unaffected. Previously we fed the RAW screen point into
+ // the box math while `off` was in untransformed layout space: the two spaces
+ // coincide only for an axis-aligned window, so on a tilted --demo corner the
+ // click landed in the wrong place. Project() is the live-path analogue of the
+ // pure-core unproject_to_local that criterion 3 verifies.
+ Rml::Vector2f p(static_cast<float>(screen_x), static_cast<float>(screen_y));
+ if (!img->Project(p)) {
+ return {}; // edge-on view: ray parallel to the element plane, no valid hit
+ }
const Rml::Vector2f off = img->GetAbsoluteOffset(Rml::BoxArea::Content);
const float bw = img->GetClientWidth();
const float bh = img->GetClientHeight();
if (bw <= 0 || bh <= 0) {
return {};
}
- const double fx = (screen_x - off.x) / bw; // 0..1 across the element box
- const double fy = (screen_y - off.y) / bh;
+ const double fx = (p.x - off.x) / bw; // 0..1 across the element box (post-projection)
+ const double fy = (p.y - off.y) / bh;
Routed out;
out.s = s;
out.sx = std::clamp(fx, 0.0, 1.0) * s->live.width;
out.sy = std::clamp(fy, 0.0, 1.0) * s->live.height;
+ if (r.debug_overlay) {
+ update_debug_marker(r, *s, win, p, out.sx, out.sy, screen_x, screen_y);
+ }
return out;
}
@@ -1224,6 +1334,27 @@ auto handle_escape_keys(Runner& r, Keyboard& kb, wlr_keyboard_key_event* ev) ->
// fall through: do not consume.
}
+ // `D` (no ctrl/alt, --demo only) -> toggle the click-accuracy debug
+ // overlay (per-surface crosshair marker + #dbg readout). Consumed so it
+ // does NOT type a 'd' into the focused client.
+ if (r.demo && !ctrl_alt && (sym == XKB_KEY_d || sym == XKB_KEY_D)) {
+ if (pressed) {
+ r.debug_overlay = !r.debug_overlay;
+ if (r.debug_overlay) {
+ // Place the marker immediately at the current cursor position.
+ (void)route_point(r, r.last_cursor_x, r.last_cursor_y);
+ slog("DEBUG overlay ON — crosshair should sit UNDER the cursor; #dbg shows "
+ "screen -> elem-local -> surface-local. Any gap = the click error.");
+ } else {
+ hide_debug_markers(r);
+ slog("DEBUG overlay OFF");
+ }
+ r.dirty = true;
+ wlr_output_schedule_frame(r.output);
+ }
+ return true; // consume press AND release; never forward
+ }
+
// Esc OR Ctrl+Alt+Backspace -> terminate the session cleanly.
if (sym == XKB_KEY_Escape || (ctrl_alt && sym == XKB_KEY_BackSpace) ||
(ctrl_alt && sym == XKB_KEY_Terminate_Server)) {
@@ -1553,6 +1684,10 @@ void handle_new_output(Runner& r, wlr_output* out) {
slog("--demo: FPS HUD centered at output centre (%dx%d square), hud_el=%p",
hud_w, hud_h, static_cast<void*>(r.hud_el));
}
+ // Grab the click-accuracy debug readout box (hidden until 'D' toggles it).
+ r.dbg_el = r.doc->GetElementById("dbg");
+ slog("--demo: click-debug overlay ready (press D to toggle), dbg_el=%p",
+ static_cast<void*>(r.dbg_el));
// Open the dedicated per-5s min/max FPS log (separate from the diagnostic
// log). fps_log_open picks $UNBOX_SPIKE_FPS_LOG else $HOME/rml-spike-fps.log.
r.fps_log = fps_log_open(r.fps_log_path);