<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>OculiX | Blog</title><description/><link>https://oculix.org/</link><language>fr</language><item><title>The engine assumed one screen. The job needed four.</title><link>https://oculix.org/fr/blog/2026-05-21-five-times-faster-parallel-vnc/</link><guid isPermaLink="true">https://oculix.org/fr/blog/2026-05-21-five-times-faster-parallel-vnc/</guid><description>I cut a 300-test visual suite from seven hours to under two — on an 8 GB VM that used to fall asleep mid-run. The fast part is the headline. The hard part was that the engine, like almost every visual-automation tool, was built for a single screen. Here&apos;s how I rebuilt isolation on top of it — and how we&apos;re going to make it native.

</description><pubDate>Fri, 22 May 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Seven hours. That’s how long a visual test suite for a Fortune 500 retail group used to take — roughly 300 tests driving point-of-sale terminals, run one after another, against a regulatory compliance window that does not move.&lt;/p&gt;
&lt;p&gt;Today it finishes in &lt;strong&gt;under two&lt;/strong&gt;. Four sessions in parallel, on a single &lt;strong&gt;8 GB Azure VM running Windows Server 2019&lt;/strong&gt; — a swap file, basic provisioning, nothing exotic. Runtime divided by five. No test farm, no per-minute cloud bill, no new hardware.&lt;/p&gt;
&lt;p&gt;The speed is the headline. It’s not the interesting part. The interesting part is that &lt;strong&gt;the engine was never built to do this&lt;/strong&gt; — and neither is almost any visual-automation tool. They assume one screen. I needed four. This is the story of the gap between those two numbers, and what I had to build to close it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-world-of-one-screen&quot;&gt;A world of one screen&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Visual automation was born in a world of one physical screen. One machine, one desktop, one cursor. SikuliX — which OculiX continues — inherited that worldview, and for fifteen years it was the &lt;em&gt;correct&lt;/em&gt; one. The engine keeps a notion of &lt;em&gt;the current screen&lt;/em&gt;, and very reasonably it keeps it in one place: shared, global state. When there is only ever one screen, a global is not a flaw. It’s the right design.&lt;/p&gt;
&lt;p&gt;That assumption is invisible right up until the moment you break it. &lt;strong&gt;VNC breaks it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A VNC session isn’t a screen — it’s a network framebuffer. No monitor, no desktop login, no &lt;code dir=&quot;auto&quot;&gt;DISPLAY&lt;/code&gt;. You can open a dozen, each on its own port, each pointing at a different remote machine. Suddenly “the current screen” stops being a singleton and becomes a question: &lt;em&gt;which one?&lt;/em&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-collision&quot;&gt;The collision&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The first time I declared two sessions and ran two test flows at once, they walked all over each other. Not with an error — worse, with quiet nonsense. Clicks landing on the wrong register. A framebuffer read mid-repaint. Results that came back green for entirely the wrong reasons.&lt;/p&gt;
&lt;p&gt;I tested it carefully, because I didn’t believe it at first. Two flows, two ports, two remote machines. They polluted each other every single time.&lt;/p&gt;
&lt;p&gt;The robot and the client were per-instance — that part was clean. But the &lt;strong&gt;port&lt;/strong&gt; and the &lt;strong&gt;registry of live sessions&lt;/strong&gt; lived in shared, static state. Two threads reaching for the same global. The single-screen assumption, baked into the data model, surfacing at exactly the moment I asked for more than one screen.&lt;/p&gt;
&lt;aside aria-label=&quot;The tell&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; The tell &lt;/p&gt;  &lt;div&gt;&lt;p&gt;During the sequential run, CPU usage stayed low the entire time. The machine wasn’t working hard — it was &lt;em&gt;queuing&lt;/em&gt;. Low CPU across a long run is never a hardware problem. It’s a scheduling problem. The box was bored.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;what-i-built--around-the-engine-not-inside-it&quot;&gt;What I built — around the engine, not inside it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;So I built the missing layer myself, on top of the engine rather than in it. Four pieces, each one fixing a failure I had actually watched happen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Isolation per flow.&lt;/strong&gt; Each test flow got its own session, bound to its own thread — its own port, its own remote, its own everything. No flow could see another’s state, because there was no shared “current session” left to see. The engine didn’t hand me thread-scoped sessions, so I made them thread-scoped from the outside.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A port is a promise.&lt;/strong&gt; Two flows must never claim the same port, and “probably free” is not good enough at startup when four of them race. So allocation became atomic: a small dedicated range, and a rule — a flow claims a port through a lock no two flows can win simultaneously, or it doesn’t run at all. A port stops being a guess and becomes a contract.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One tunnel per session.&lt;/strong&gt; Each remote register was reachable only through an SSH tunnel exposing its framebuffer on a local port. One tunnel per flow, opened on demand, torn down after — and, the detail that cost me a late night, &lt;em&gt;closed before the next one opened&lt;/em&gt;. Orphaned tunnels hold ports hostage long after the process that spawned them is gone. Close-before-open, not close-after. Order matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Readiness, not optimism.&lt;/strong&gt; The engine waited a fixed few seconds for a session to come up and hoped for the best. Hope does not survive four sessions starting at once. So I waited for &lt;em&gt;proof&lt;/em&gt; instead: the &lt;strong&gt;RFB banner&lt;/strong&gt; — the three bytes a VNC server sends to announce it’s ready for pixels. No banner, no test. A probe, not a sleep.&lt;/p&gt;
&lt;aside aria-label=&quot;The shape of the fix&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; The shape of the fix &lt;/p&gt;  &lt;div&gt;&lt;p&gt;Notice what all four pieces have in common: they each take something the engine held as &lt;em&gt;one global thing&lt;/em&gt; and turn it into &lt;em&gt;one thing per flow&lt;/em&gt;. That’s the whole trick. Parallelism here wasn’t about adding threads — it was about removing shared state.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;why-four-sessions-gave-5-not-4&quot;&gt;Why four sessions gave 5×, not 4×&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Four workers, five times faster, looks like broken arithmetic. It isn’t.&lt;/p&gt;
&lt;p&gt;The killer in a sequential suite isn’t the total work — it’s the &lt;strong&gt;variance in test durations&lt;/strong&gt;. My ~300 tests ranged from a few seconds to many minutes. Run in a single line, a long test sits in front of forty short ones and they all just wait. Sequential execution doesn’t only pay for the work; it pays for every test queued behind a slower one. With uneven durations that waiting tax is enormous, and invisible, because the CPU is idle the whole time.&lt;/p&gt;
&lt;p&gt;I split the load into batches sized by &lt;strong&gt;functional area&lt;/strong&gt; — thirty tests here, ninety there, a hundred and twenty somewhere else, the way the business actually reasons about its registers. Balanced, not just parallel. The four sessions finished at roughly the same time instead of one straggler holding the line.&lt;/p&gt;
&lt;p&gt;So the 5× isn’t four workers doing 4× the work. It’s four balanced workers &lt;strong&gt;deleting waste&lt;/strong&gt; the single queue was silently burning. When the thing you replaced is that wasteful, even modest, well-distributed parallelism overshoots the naive ratio.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-all-of-this-lived-outside-the-engine&quot;&gt;Why all of this lived outside the engine&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I want to be precise here, out of respect for the engine and the people who built it. The core does one thing extremely well: clean, reliable, single-session VNC. It kept a single-screen promise and it kept it honestly. The orchestration — the thread isolation, the atomic ports, the tunnels, the readiness probe — was &lt;em&gt;mine&lt;/em&gt;, and it lived in my harness. I made a different promise, one layer up, and kept that one too.&lt;/p&gt;
&lt;p&gt;For a long time that was the right division of labour. The engine stayed pure; the parallelism was an application concern. But once you’ve named the gap, you can’t un-see it — and you start to wonder why the engine shouldn’t simply do this itself.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-roadmap-making-parallel-sessions-first-class&quot;&gt;The roadmap: making parallel sessions first-class&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s where it gets exciting, because the next step is obvious now that the gap has a name. &lt;strong&gt;Everything I built outside the engine is the engine’s natural next form.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.838 10.302L22.270 10.302L22.270 23.979L1.730 23.979L1.730 10.302L5.162 10.302L5.162 6.918Q5.162 6.354 5.255 5.884L5.255 5.884Q5.490 4.051 6.642 2.618Q7.793 1.184 9.509 0.503Q11.224-0.179 13.104 0.103L13.104 0.103Q15.548 0.526 17.194 2.453Q18.838 4.380 18.838 6.871L18.838 6.871L18.838 10.302ZM8.545 10.302L15.407 10.302Q15.407 10.255 15.407 10.208L15.407 10.208L15.407 6.777Q15.407 6.542 15.360 6.213L15.360 6.213Q15.078 4.850 14.021 4.098Q12.963 3.346 11.553 3.487L11.553 3.487Q10.284 3.581 9.415 4.592Q8.545 5.602 8.545 6.918L8.545 6.918L8.545 10.302Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Thread-scoped sessions&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;No more global “current screen.” A session belongs to the flow that opened it, by construction — so two flows can never reach for the same state, because there is no shared state to reach for.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M23.165 13.645L23.165 13.645Q22.829 13.561 22.199 13.309L22.199 13.309L21.695 13.099Q21.695 12.637 21.632 11.797Q21.569 10.957 21.569 10.495L21.569 10.495Q21.569 10.411 21.632 10.348Q21.695 10.285 21.695 10.201L21.695 10.201L23.165 9.571Q23.543 9.361 23.669 9.004Q23.795 8.647 23.669 8.269L23.669 8.269L22.493 5.623Q22.283 5.203 21.926 5.056Q21.569 4.909 21.191 5.119L21.191 5.119L19.721 5.749Q19.469 5.749 19.469 5.623L19.469 5.623Q19.301 5.371 18.839 4.951L18.839 4.951L18.545 4.699Q18.419 4.573 18.104 4.300Q17.789 4.027 17.621 3.901L17.621 3.901Q17.789 3.649 17.978 3.124Q18.167 2.599 18.293 2.347L18.293 2.347Q18.503 1.843 18.314 1.465Q18.125 1.087 17.621 0.919L17.621 0.919Q17.075 0.793 15.815 0.373L15.815 0.373L15.143 0.121Q14.639-0.089 14.261 0.100Q13.883 0.289 13.715 0.751L13.715 0.751Q13.631 1.087 13.379 1.717L13.379 1.717L13.169 2.221Q12.707 2.221 11.867 2.284Q11.027 2.347 10.565 2.347L10.565 2.347Q10.481 2.347 10.418 2.284Q10.355 2.221 10.271 2.221L10.271 2.221L9.641 0.751Q9.431 0.373 9.074 0.247Q8.717 0.121 8.339 0.247L8.339 0.247L5.693 1.423Q5.273 1.633 5.126 1.990Q4.979 2.347 5.189 2.725L5.189 2.725L5.819 4.195Q5.819 4.447 5.693 4.447L5.693 4.447L5.399 4.699Q5.105 4.909 5.021 5.077L5.021 5.077Q4.853 5.287 4.517 5.686Q4.181 6.085 3.971 6.295L3.971 6.295Q3.677 6.211 3.047 5.959L3.047 5.959L2.543 5.749Q2.039 5.539 1.661 5.728Q1.283 5.917 1.115 6.421L1.115 6.421L0.905 6.925Q0.401 8.185 0.191 8.773Q-0.019 9.361 0.128 9.697Q0.275 10.033 0.821 10.201L0.821 10.201Q1.157 10.285 1.787 10.537L1.787 10.537L2.291 10.747Q2.291 11.209 2.354 12.049Q2.417 12.889 2.417 13.351L2.417 13.351Q2.417 13.435 2.354 13.498Q2.291 13.561 2.291 13.645L2.291 13.645L0.821 14.275Q0.443 14.485 0.317 14.842Q0.191 15.199 0.317 15.577L0.317 15.577L1.493 18.223Q1.703 18.643 2.060 18.790Q2.417 18.937 2.795 18.727L2.795 18.727L4.265 18.097Q4.517 18.097 4.517 18.223L4.517 18.223Q4.685 18.475 5.147 18.895L5.147 18.895L5.441 19.147Q5.567 19.273 5.882 19.525Q6.197 19.777 6.365 19.945L6.365 19.945Q6.239 20.197 6.029 20.722Q5.819 21.247 5.693 21.499L5.693 21.499Q5.483 22.003 5.672 22.381Q5.861 22.759 6.365 22.969L6.365 22.969Q6.659 23.053 7.331 23.305L7.331 23.305Q8.297 23.725 8.864 23.893Q9.431 24.061 9.767 23.935Q10.103 23.809 10.271 23.221L10.271 23.221Q10.355 22.885 10.607 22.255L10.607 22.255L10.817 21.751Q11.279 21.751 12.119 21.688Q12.959 21.625 13.421 21.625L13.421 21.625Q13.505 21.625 13.568 21.688Q13.631 21.751 13.715 21.751L13.715 21.751L14.345 23.221Q14.555 23.599 14.912 23.725Q15.269 23.851 15.647 23.725L15.647 23.725L18.293 22.549Q18.713 22.339 18.860 21.982Q19.007 21.625 18.797 21.247L18.797 21.247L18.167 19.819Q18.167 19.525 18.293 19.525L18.293 19.525Q18.545 19.357 18.965 18.895L18.965 18.895L19.217 18.601Q19.343 18.475 19.595 18.160Q19.847 17.845 20.015 17.719L20.015 17.719Q20.309 17.761 20.939 18.055L20.939 18.055L21.443 18.223Q21.947 18.433 22.325 18.244Q22.703 18.055 22.871 17.551L22.871 17.551Q22.997 17.257 23.249 16.585L23.249 16.585Q23.669 15.619 23.795 15.073L23.795 15.073Q24.257 13.981 23.165 13.645ZM13.967 16.375L13.967 16.375Q12.077 17.173 10.250 16.459Q8.423 15.745 7.541 13.897L7.541 13.897Q6.743 12.007 7.457 10.180Q8.171 8.353 10.019 7.471L10.019 7.471Q11.909 6.673 13.736 7.387Q15.563 8.101 16.445 9.949L16.445 9.949Q17.243 11.839 16.529 13.666Q15.815 15.493 13.967 16.375Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Port allocation, built in&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;The engine hands you an isolated session and owns the atomic port claim itself. No range to reserve, no lock-files to babysit, no collisions to debug at 23h.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Readiness on the RFB banner&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;The optimistic startup sleep replaced by a real probe — the session reports ready when the server actually says it’s ready, not when a timer guesses it might be.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M1.44 8.855v-.001l3.527-3.516c.34-.344.802-.541 1.285-.548h6.649l.947-.947c3.07-3.07 6.207-3.072 7.62-2.868a1.821 1.821 0 0 1 1.557 1.557c.204 1.413.203 4.55-2.868 7.62l-.946.946v6.649a1.845 1.845 0 0 1-.549 1.286l-3.516 3.528a1.844 1.844 0 0 1-3.11-.944l-.858-4.275-4.52-4.52-2.31-.463-1.964-.394A1.847 1.847 0 0 1 .98 10.693a1.843 1.843 0 0 1 .46-1.838Zm5.379 2.017-3.873-.776L6.32 6.733h4.638l-4.14 4.14Zm8.403-5.655c2.459-2.46 4.856-2.463 5.89-2.33.134 1.035.13 3.432-2.329 5.891l-6.71 6.71-3.561-3.56 6.71-6.711Zm-1.318 15.837-.776-3.873 4.14-4.14v4.639l-3.364 3.374Z&quot; clip-rule=&quot;evenodd&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M9.318 18.345a.972.972 0 0 0-1.86-.561c-.482 1.435-1.687 2.204-2.934 2.619a8.22 8.22 0 0 1-1.23.302c.062-.365.157-.79.303-1.229.415-1.247 1.184-2.452 2.62-2.935a.971.971 0 1 0-.62-1.842c-.12.04-.236.084-.35.13-2.02.828-3.012 2.588-3.493 4.033a10.383 10.383 0 0 0-.51 2.845l-.001.016v.063c0 .536.434.972.97.972H2.24a7.21 7.21 0 0 0 .878-.065c.527-.063 1.248-.19 2.02-.447 1.445-.48 3.205-1.472 4.033-3.494a5.828 5.828 0 0 0 .147-.407Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;A clean SSH tunnel, already shipped&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A pure-Java tunnel (no WSL, no external &lt;code&gt;sshpass&lt;/code&gt;) already lives in the core. The next step is wiring it into a documented, first-class parallel pattern.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;The goal for the next major version is simple to state and worth the work: &lt;strong&gt;declare N sessions, get N isolated sessions.&lt;/strong&gt; No harness, no lock-files, no late nights over ghost tunnels. The single-screen assumption retired — gently, and with full respect for the fifteen years it served exactly right.&lt;/p&gt;
&lt;p&gt;OculiX is MIT, open source, built in the open. If you drive screens for a living — registers, kiosks, terminals, application instances by the dozen — this is the corner of the roadmap I’d most like company on.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Visual automation has a reputation for not scaling. It earned that reputation honestly: the tools were built for one screen, and most of them still are. But the limit was never the pixels, and it was never the matching. It was an &lt;strong&gt;assumption&lt;/strong&gt; — one screen — that nobody had yet needed to question.&lt;/p&gt;
&lt;p&gt;Question it. Give the engine real sessions, isolate them, balance the load across uneven work — and a bored 8 GB VM does in two hours what used to take seven. No farm. No new hardware. No per-minute bill.&lt;/p&gt;
&lt;p&gt;The screens were never the bottleneck. The assumption was.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;OculiX is open-source under the MIT license. The parallel VNC engine — and the work to make multi-session parallelism native in the core — is described in the &lt;a href=&quot;https://oculix.org/guides/parallel-vnc/&quot;&gt;Parallel VNC execution guide&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>vnc</category><category>parallelization</category><category>performance</category><category>visual-testing</category><category>architecture</category><category>retrospective</category></item><item><title>CodeQL: the boy who cried Error</title><link>https://oculix.org/fr/blog/2026-05-21-codeql-cried-error-19-findings-zero-impact/</link><guid isPermaLink="true">https://oculix.org/fr/blog/2026-05-21-codeql-cried-error-19-findings-zero-impact/</guid><description>Static analysis classified 19 findings as Error on a 17-year-old Java codebase. After triage: zero observed user impact in fifteen years. Meanwhile, 53 NumberFormatException catches missing in production code are classified Note. The severity system is broken in both directions.

</description><pubDate>Thu, 21 May 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We enabled GitHub CodeQL on OculiX in April 2026. The repository inherits sixteen years of Sikuli and SikuliX history before our continuation began, so the scanner was pointed at roughly 110 000 lines of Java accumulated since 2009.&lt;/p&gt;
&lt;p&gt;It returned a hundred or so findings across all severities. Nineteen of them were classified &lt;strong&gt;Error&lt;/strong&gt;, the highest tier. The classification suggests urgency: each Error is, in CodeQL’s taxonomy, the kind of thing a maintainer should drop everything to fix.&lt;/p&gt;
&lt;p&gt;We dropped nothing. We triaged each one. After triage, the result is unambiguous: &lt;strong&gt;zero of the nineteen findings have caused an observable user incident in fifteen years&lt;/strong&gt;. Most of them are dormant architectural defects, dead code paths, or false positives produced by structural blindness in the analyzer.&lt;/p&gt;
&lt;p&gt;Meanwhile, &lt;strong&gt;fifty-three missing catches for &lt;code dir=&quot;auto&quot;&gt;NumberFormatException&lt;/code&gt;&lt;/strong&gt; — places where parsing a user-provided number can crash the application — are classified &lt;code dir=&quot;auto&quot;&gt;Note&lt;/code&gt;. Almost invisible. Below &lt;code dir=&quot;auto&quot;&gt;Warning&lt;/code&gt;. Far below &lt;code dir=&quot;auto&quot;&gt;Error&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This post walks through the nineteen findings, explains why none of them actually warrant the Error tier, and explains why the things that should warrant it are buried two levels down. The conclusion is not that static analysis is useless. The conclusion is that &lt;strong&gt;CodeQL’s severity classification is broken in both directions&lt;/strong&gt; — overstating the trivial, understating the serious — and that the noise this generates is the main thing keeping the tool from being useful at scale.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;methodology&quot;&gt;Methodology&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For each finding, we asked three questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Does it produce an observable user incident?&lt;/strong&gt; Has it caused a crash, a wrong result, a regression in fifteen years of use? Has anyone filed a ticket against it?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If not, would it produce one if a specific code path were exercised?&lt;/strong&gt; Is it dormant but reachable, or dormant and unreachable?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is the finding even correct?&lt;/strong&gt; Does CodeQL’s analysis actually match the runtime behavior, or has it missed something about the language, the bridge to another runtime, or the type system?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Three categories emerged from this triage:&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M12 16a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm10.67 1.47-8.05-14a3 3 0 0 0-5.24 0l-8 14A3 3 0 0 0 3.94 22h16.12a3 3 0 0 0 2.61-4.53Zm-1.73 2a1 1 0 0 1-.88.51H3.94a1 1 0 0 1-.88-.51 1 1 0 0 1 0-1l8-14a1 1 0 0 1 1.78 0l8.05 14a1 1 0 0 1 .05 1.02v-.02ZM12 8a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V9a1 1 0 0 0-1-1Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Architectural defects&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Real problems in the code that have never been triggered. Either the branch is dead, the configuration is never set, or the upstream dependency is never invoked. The finding is correct but the impact is theoretical.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M4.314 0.198L16.746 0.198Q17.838 0.198 18.615 0.975Q19.392 1.752 19.392 2.844L19.392 2.844L19.392 21.156Q19.392 22.248 18.615 23.025Q17.838 23.802 16.746 23.802L16.746 23.802L4.314 23.802Q3.222 23.802 2.445 23.025Q1.668 22.248 1.668 21.156L1.668 21.156L1.668 2.844Q1.668 1.752 2.445 0.975Q3.222 0.198 4.314 0.198L4.314 0.198ZM21.450 15.528L20.568 15.528L21.450 15.528Q21.786 15.528 22.038 15.759Q22.290 15.990 22.332 16.326L22.332 16.326L22.332 18.216Q22.332 18.552 22.122 18.783Q21.912 19.014 21.576 19.098L21.576 19.098L20.568 19.098L20.568 15.528L21.450 15.528ZM21.450 10.824L20.568 10.824L21.450 10.824Q21.786 10.824 22.038 11.034Q22.290 11.244 22.332 11.580L22.332 11.580L22.332 13.470Q22.332 13.806 22.122 14.058Q21.912 14.310 21.576 14.352L21.576 14.352L20.568 14.352L20.568 10.824L21.450 10.824ZM21.450 6.078L20.568 6.078L21.450 6.078Q21.786 6.078 22.038 6.309Q22.290 6.540 22.332 6.876L22.332 6.876L22.332 8.766Q22.332 9.102 22.122 9.333Q21.912 9.564 21.576 9.648L21.576 9.648L20.568 9.648L20.568 6.078L21.450 6.078ZM14.352 4.314L14.352 4.314L6.708 4.314Q6.372 4.314 6.120 4.545Q5.868 4.776 5.826 5.070L5.826 5.070L5.784 7.002Q5.784 7.296 6.015 7.548Q6.246 7.800 6.582 7.842L6.582 7.842L14.352 7.884Q14.688 7.884 14.940 7.653Q15.192 7.422 15.234 7.086L15.234 7.086L15.276 5.196Q15.276 4.818 15.003 4.566Q14.730 4.314 14.352 4.314Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Code smells&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Patterns that look suspicious but have legitimate uses (or, more often, have just persisted as harmless cruft for over a decade). The code is ugly but not broken.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M12 7a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1Zm0 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm9.71-7.44-5.27-5.27a1.05 1.05 0 0 0-.71-.29H8.27a1.05 1.05 0 0 0-.71.29L2.29 7.56a1.05 1.05 0 0 0-.29.71v7.46c.004.265.107.518.29.71l5.27 5.27c.192.183.445.286.71.29h7.46a1.05 1.05 0 0 0 .71-.29l5.27-5.27a1.05 1.05 0 0 0 .29-.71V8.27a1.05 1.05 0 0 0-.29-.71ZM20 15.31 15.31 20H8.69L4 15.31V8.69L8.69 4h6.62L20 8.69v6.62Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;False positives&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;CodeQL’s analysis is wrong. It missed a dynamic access, a language bridge, an inheritance relationship, or a type guarantee. The finding does not correspond to any real risk in the code.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;What we did &lt;strong&gt;not&lt;/strong&gt; find in our nineteen Errors: any case where a user reproduced a crash, a wrong result, or a regression caused by the code at the location CodeQL flagged. Not one.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-tour-of-the-nineteen-errors&quot;&gt;A tour of the nineteen Errors&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Below is the breakdown by CodeQL rule, in descending count.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;container-contents-are-never-accessed-8-findings&quot;&gt;Container contents are never accessed (8 findings)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;CodeQL’s claim: a collection or map is allocated and possibly written to, but its contents are never read back. The implication is that the container is dead code that should be removed.&lt;/p&gt;
&lt;p&gt;Eight findings of this kind landed on our codebase. The triage:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three are genuine dead code.&lt;/strong&gt; Two local variables named &lt;code dir=&quot;auto&quot;&gt;classpath&lt;/code&gt; inside &lt;code dir=&quot;auto&quot;&gt;ExtensionManager.java&lt;/code&gt;, and a local map &lt;code dir=&quot;auto&quot;&gt;docParams&lt;/code&gt; inside &lt;code dir=&quot;auto&quot;&gt;Crawler.java&lt;/code&gt;. All three are declared, populated in a loop, and never consumed. Cleanup candidates. None of them have produced a single user-visible defect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Four are false positives caused by dynamic access.&lt;/strong&gt; Lists like &lt;code dir=&quot;auto&quot;&gt;aliases&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;filenames&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;mimeTypes&lt;/code&gt; in &lt;code dir=&quot;auto&quot;&gt;Lexer.java&lt;/code&gt; (the syntax highlighter grammar) are populated by parsing grammar metadata and consumed by a registry mechanism that CodeQL cannot trace. A map called &lt;code dir=&quot;auto&quot;&gt;libsLoaded&lt;/code&gt; in &lt;code dir=&quot;auto&quot;&gt;RunTime.java&lt;/code&gt; tracks loaded native libraries and is consulted before each native load — but the consumption goes through a debug helper that CodeQL’s call-graph analysis does not follow. From the static point of view, the containers look unread. From the runtime point of view, they are read on every invocation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One is the most interesting finding in the entire set.&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;SikulixServer.allowedIPs&lt;/code&gt; is declared like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;allowedIPs&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&amp;#x3C;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The class is the Sikuli network server. The list is named after a security feature: an IP allow-list to restrict who can connect. There is a method called &lt;code dir=&quot;auto&quot;&gt;makeAllowedIPs&lt;/code&gt; that populates the list. &lt;strong&gt;There is no code that reads the list.&lt;/strong&gt; No filter checks against it. No connection-handling code consults it. The allow-list is built but never enforced.&lt;/p&gt;
&lt;p&gt;If an administrator configures &lt;code dir=&quot;auto&quot;&gt;allowedIPs&lt;/code&gt; thinking they are restricting access, &lt;strong&gt;they are not&lt;/strong&gt;. The server accepts every connection regardless of the configured allow-list, because the list is allocated, filled, and never read.&lt;/p&gt;
&lt;p&gt;This is a security defect. A serious one, by any reasonable definition. CodeQL classifies it as &lt;strong&gt;Maintainability — Error&lt;/strong&gt;. Not Security. Not Critical. Just “your container is unused”, as if the impact were cosmetic.&lt;/p&gt;
&lt;p&gt;We have opened an internal issue to investigate whether the network server is reachable in any default OculiX deployment, and to either remove the network server entirely or wire up the missing filter. As of this writing, the defect has not produced an incident report in any of the years it has existed — but that is more an accident of the network server’s low usage than a vindication of CodeQL’s classification.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;self-assignment-2-findings&quot;&gt;Self assignment (2 findings)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;CodeQL’s claim: a variable is being assigned to itself. The assignment has no effect.&lt;/p&gt;
&lt;p&gt;The first finding is in &lt;code dir=&quot;auto&quot;&gt;Runner.java:396&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setLastScriptRunReturnCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; lastScriptRunReturnCode&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lastScriptRunReturnCode &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; lastScriptRunReturnCode;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The setter’s parameter shadows the static field. The author intended &lt;code dir=&quot;auto&quot;&gt;Runner.lastScriptRunReturnCode = lastScriptRunReturnCode;&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;this.lastScriptRunReturnCode = lastScriptRunReturnCode;&lt;/code&gt;. As written, the setter is inert. It has been inert since whenever it was added — almost certainly in the mid-2010s, possibly earlier.&lt;/p&gt;
&lt;p&gt;For fifteen years, any caller invoking &lt;code dir=&quot;auto&quot;&gt;Runner.setLastScriptRunReturnCode(5)&lt;/code&gt; has stored exactly nothing. The field &lt;code dir=&quot;auto&quot;&gt;lastScriptRunReturnCode&lt;/code&gt; stays at its initial value of &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; forever. Anyone reading the field gets &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; regardless of what scripts have actually returned.&lt;/p&gt;
&lt;p&gt;Has anyone noticed? Apparently not. Either the field is never consulted, or the consumers do not depend on the value being non-zero, or the corner cases where it would matter have not been hit in fifteen years of cumulative production use. The “bug” is real in the source code, but the &lt;strong&gt;observable behavior is no different from what it would be if the setter worked correctly&lt;/strong&gt;. The downstream code path has adapted to the dysfunction.&lt;/p&gt;
&lt;p&gt;The second finding is in &lt;code dir=&quot;auto&quot;&gt;Deflate.java:1703&lt;/code&gt;, inside &lt;code dir=&quot;auto&quot;&gt;com.jcraft.jzlib&lt;/code&gt; (the JZlib compression library, embedded for SSH purposes):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Deflate&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (Deflate) &lt;/span&gt;&lt;span&gt;super&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clone&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pending_buf&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;pending_buf&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;d_buf&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;d_buf&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;l_buf&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;l_buf&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;dest&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;clone()&lt;/code&gt; method deep-duplicates every buffer except &lt;code dir=&quot;auto&quot;&gt;d_buf&lt;/code&gt;. The assignment &lt;code dir=&quot;auto&quot;&gt;dest.d_buf = dest.d_buf;&lt;/code&gt; is a typo where &lt;code dir=&quot;auto&quot;&gt;dup(dest.d_buf)&lt;/code&gt; was intended. The result is that the cloned &lt;code dir=&quot;auto&quot;&gt;Deflate&lt;/code&gt; instance shares its &lt;code dir=&quot;auto&quot;&gt;d_buf&lt;/code&gt; with the original. Modifying the buffer through either reference is visible through both.&lt;/p&gt;
&lt;p&gt;The defect is in an upstream library that nobody has actively maintained for years. The OculiX project carries it because the library is bundled. The risk is real but only materializes if the &lt;code dir=&quot;auto&quot;&gt;Deflate.clone()&lt;/code&gt; method is called in a context where both the original and the clone are used concurrently — a rare pattern that JZlib’s tests apparently do not cover and that nobody seems to hit in production.&lt;/p&gt;
&lt;p&gt;Both Self assignment findings are genuinely correct. Both have been silently dormant in the codebase for over a decade. &lt;strong&gt;Neither has produced a user-visible incident.&lt;/strong&gt; CodeQL classifies them as &lt;strong&gt;Error&lt;/strong&gt;. The classification suggests they require immediate attention. The empirical evidence suggests they have been irrelevant for a decade.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;reference-equality-test-of-boxed-types-1-finding&quot;&gt;Reference equality test of boxed types (1 finding)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;RecordedEventsFlow.java:149&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;modifiers&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; characterWithModifiers &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; character) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;actions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TypeTextAction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;keyText, modifiersTexts&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;characterWithModifiers&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;character&lt;/code&gt; are boxed &lt;code dir=&quot;auto&quot;&gt;Integer&lt;/code&gt; (or &lt;code dir=&quot;auto&quot;&gt;Character&lt;/code&gt;) values. Comparing them with &lt;code dir=&quot;auto&quot;&gt;==&lt;/code&gt; compares object references, not values. Java caches boxed integers from -128 to 127, so for any ASCII codepoint, two boxed instances of the same value point to the same cached object, and &lt;code dir=&quot;auto&quot;&gt;==&lt;/code&gt; returns &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; by accident. For codepoints above 127 — accented Latin, CJK, Cyrillic, Arabic, Hindi, almost any non-ASCII script — the cache does not apply, and &lt;code dir=&quot;auto&quot;&gt;==&lt;/code&gt; returns &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt; even when the values are equal.&lt;/p&gt;
&lt;p&gt;The defect is correct. It has existed for years. The empirical impact is invisible because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The recorder is used predominantly with ASCII input in the regions where it is most used.&lt;/li&gt;
&lt;li&gt;When the comparison fails for non-ASCII input, the recorder falls through to a different branch that produces a slightly different — but still functional — action.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a textbook example of a real defect that has been silently absorbed by the surrounding code’s tolerance. No user has filed a ticket. No CI test has caught it. The fix is one line (&lt;code dir=&quot;auto&quot;&gt;.equals()&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;==&lt;/code&gt;). The classification as Error is consistent with CodeQL’s rule but &lt;strong&gt;inconsistent with the actual user impact&lt;/strong&gt;, which is zero.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;equals-method-does-not-inspect-argument-type-1-finding&quot;&gt;Equals method does not inspect argument type (1 finding)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;PatternPaneScreenshot.java:36&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt; o&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;CodeQL warns that the method does not check the argument’s type, and might therefore lead to a &lt;code dir=&quot;auto&quot;&gt;ClassCastException&lt;/code&gt; somewhere downstream. The implementation is trivial: it returns &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt; for any input, including &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt;. There is no cast. There is no downstream usage of the result that depends on the argument’s type.&lt;/p&gt;
&lt;p&gt;The override exists inside a &lt;code dir=&quot;auto&quot;&gt;Comparator&lt;/code&gt; declaration. The &lt;code dir=&quot;auto&quot;&gt;Comparator&lt;/code&gt; interface allows &lt;code dir=&quot;auto&quot;&gt;equals&lt;/code&gt; to be overridden to indicate that two comparators impose the same ordering. Returning &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt; is a safe, conservative default: “I do not claim to impose the same ordering as you do.” It does no harm.&lt;/p&gt;
&lt;p&gt;The finding is a false positive on this specific code. The override is pointless (it could be removed without changing behavior), but it is not a defect. CodeQL classifies it as &lt;strong&gt;Reliability — Error&lt;/strong&gt;, suggesting it could cause a crash. It cannot. The implementation has no crash path.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;non-callable-called-2-findings-python&quot;&gt;Non-callable called (2 findings, Python)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Two findings on &lt;code dir=&quot;auto&quot;&gt;Sikuli.py&lt;/code&gt;, the Jython-based scripting layer that ships with OculiX. Both report that an imported name is being called as if it were callable when it is not.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; org.sikuli.script.ScreenRemote &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SR&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SCREEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SR&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;adr&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The CodeQL Python analyzer treats this as CPython. It sees &lt;code dir=&quot;auto&quot;&gt;import org.sikuli.script.ScreenRemote&lt;/code&gt; as importing an attribute, has no idea that &lt;code dir=&quot;auto&quot;&gt;ScreenRemote&lt;/code&gt; is a Java class, and concludes that calling &lt;code dir=&quot;auto&quot;&gt;SR(...)&lt;/code&gt; is invalid.&lt;/p&gt;
&lt;p&gt;In Jython, this import is valid Java-to-Python bridging. &lt;code dir=&quot;auto&quot;&gt;SR(...)&lt;/code&gt; invokes the Java constructor &lt;code dir=&quot;auto&quot;&gt;ScreenRemote(String, String)&lt;/code&gt;. The runtime knows this. CodeQL does not.&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;structural blindness across language runtimes&lt;/strong&gt;. CodeQL Python has no awareness of the Jython platform. Every Java class accessed from a Jython script will be flagged as “non-callable called”. The number of such findings will grow linearly with the size of the scripting layer.&lt;/p&gt;
&lt;p&gt;Both findings are false positives. There is no fix on the project side. The recourse is to disable the rule in &lt;code dir=&quot;auto&quot;&gt;.codeql.yml&lt;/code&gt; or to suppress the findings manually one by one.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;dereferenced-variable-is-always-null-1-finding&quot;&gt;Dereferenced variable is always null (1 finding)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;ButtonGenCommand.java:127&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;pref&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAutoCaptureForCmdButtons&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;btnCapture &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;//TODO new ButtonCapture(pane, line);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;pane&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insertComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;btnCapture&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;btnCapture&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;captureWithAutoDelay&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The author explicitly assigned &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt;, left a &lt;code dir=&quot;auto&quot;&gt;TODO&lt;/code&gt; comment, and then dereferenced the variable twice. Whoever opens the IDE with the &lt;code dir=&quot;auto&quot;&gt;AutoCaptureForCmdButtons&lt;/code&gt; preference enabled will crash on &lt;code dir=&quot;auto&quot;&gt;btnCapture.captureWithAutoDelay()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is the only finding in the set that is &lt;strong&gt;a definite crash trap&lt;/strong&gt;. CodeQL is correct to flag it. Calling it &lt;code dir=&quot;auto&quot;&gt;Error&lt;/code&gt; is, for once, defensible.&lt;/p&gt;
&lt;p&gt;Has anyone hit the crash? The preference is off by default, and the &lt;code dir=&quot;auto&quot;&gt;TODO&lt;/code&gt; has lived in the code for years, which suggests nobody enables this preference in practice. The crash is a real risk but a dormant one. Fix: replace the &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;new ButtonCapture(pane, line)&lt;/code&gt; and finish the TODO.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;contradictory-type-checks-1-finding&quot;&gt;Contradictory type checks (1 finding)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;Offset.java:152&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;RMILO&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Offset&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;RMILO&lt;/span&gt;&lt;span&gt; whatEver&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (whatEver &lt;/span&gt;&lt;span&gt;instanceof&lt;/span&gt;&lt;span&gt; Region &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; whatEver &lt;/span&gt;&lt;span&gt;instanceof&lt;/span&gt;&lt;span&gt; Match) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Region&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;what&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (Region) whatEver;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;CodeQL claims that when &lt;code dir=&quot;auto&quot;&gt;whatEver&lt;/code&gt; is &lt;code dir=&quot;auto&quot;&gt;Match&lt;/code&gt; but not &lt;code dir=&quot;auto&quot;&gt;Region&lt;/code&gt;, the cast to &lt;code dir=&quot;auto&quot;&gt;Region&lt;/code&gt; will fail at runtime. This would be true if &lt;code dir=&quot;auto&quot;&gt;Match&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;Region&lt;/code&gt; were unrelated types. They are not. &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Match&lt;/code&gt; extends &lt;code dir=&quot;auto&quot;&gt;Region&lt;/code&gt;&lt;/strong&gt; throughout the Sikuli class hierarchy. Every &lt;code dir=&quot;auto&quot;&gt;Match&lt;/code&gt; is also a &lt;code dir=&quot;auto&quot;&gt;Region&lt;/code&gt;. The cast is safe.&lt;/p&gt;
&lt;p&gt;CodeQL does not consult the inheritance graph of the project, so it treats the two types as disjoint. The OR condition is redundant (&lt;code dir=&quot;auto&quot;&gt;whatEver instanceof Region&lt;/code&gt; already covers the &lt;code dir=&quot;auto&quot;&gt;Match&lt;/code&gt; case), but it is not unsafe.&lt;/p&gt;
&lt;p&gt;The finding is a false positive caused by &lt;strong&gt;type system blindness&lt;/strong&gt;. The check on &lt;code dir=&quot;auto&quot;&gt;Match&lt;/code&gt; is dead, in the sense that it is always reached on objects that already match &lt;code dir=&quot;auto&quot;&gt;instanceof Region&lt;/code&gt;. Removing it is a cleanup, not a bug fix.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;type-mismatch-on-container-modification-1-finding&quot;&gt;Type mismatch on container modification (1 finding)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;HotkeyManager.java:263&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_instance&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_removeHotkey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key, mod&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (res) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;hotkeys&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;hotkeys&lt;/code&gt; is a &lt;code dir=&quot;auto&quot;&gt;Map&lt;/code&gt; keyed on &lt;code dir=&quot;auto&quot;&gt;String&lt;/code&gt;. &lt;code dir=&quot;auto&quot;&gt;key&lt;/code&gt; is &lt;code dir=&quot;auto&quot;&gt;Integer&lt;/code&gt; (an AWT virtual key code). &lt;code dir=&quot;auto&quot;&gt;Map.remove(Object)&lt;/code&gt; accepts any &lt;code dir=&quot;auto&quot;&gt;Object&lt;/code&gt; for backward compatibility with pre-generics Java, so the call compiles cleanly. At runtime, no entry in the map has an &lt;code dir=&quot;auto&quot;&gt;Integer&lt;/code&gt; key, so the remove silently does nothing. The intended key for the map is a textual representation that must be constructed from &lt;code dir=&quot;auto&quot;&gt;key&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;mod&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The hotkey is reported as removed (because &lt;code dir=&quot;auto&quot;&gt;_removeHotkey&lt;/code&gt; succeeded at unbinding the OS-level listener), but the entry stays in the map indefinitely. If &lt;code dir=&quot;auto&quot;&gt;hotkeys&lt;/code&gt; is consulted to dispatch incoming events, &lt;strong&gt;the old hotkey’s listener continues to fire&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The defect is real and observable in theory. In practice, no user has filed a ticket about a hotkey persisting after rebind. Either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The listener-dispatch path does not consult the map.&lt;/li&gt;
&lt;li&gt;Nobody rebinds hotkeys.&lt;/li&gt;
&lt;li&gt;The fallout is silently absorbed by other code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Whichever explanation applies, &lt;strong&gt;the failure mode of this defect is dormant in the same way as the others&lt;/strong&gt;. CodeQL’s classification as Error matches the technical risk but overstates the empirical urgency.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;missing-format-argument-2-findings&quot;&gt;Missing format argument (2 findings)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;Commons.java:1525&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;RunTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;terminate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;999&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Commons.loadLib: %s&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The format string demands an argument, none is supplied. When this line is reached, the &lt;code dir=&quot;auto&quot;&gt;String.format&lt;/code&gt; call throws &lt;code dir=&quot;auto&quot;&gt;IllegalFormatException&lt;/code&gt;. The thrown exception is wrapped inside the &lt;code dir=&quot;auto&quot;&gt;terminate&lt;/code&gt; call, which means the user sees a confusing format-related error instead of a clean shutdown with code 999 and a useful diagnostic message.&lt;/p&gt;
&lt;p&gt;The branch is an error path inside the native library loading code. It executes when a library fails to load — itself a rare event. The defect aggravates the user experience precisely at the moment when it is already poor (something is breaking, and now the error reporting is also breaking), but it does not produce its own incident on a healthy system.&lt;/p&gt;
&lt;p&gt;The second finding is &lt;code dir=&quot;auto&quot;&gt;Crawler.java:441&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docMethod &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;format&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pyMethod, clazz, method, docParams, returns&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The format string &lt;code dir=&quot;auto&quot;&gt;pyMethod&lt;/code&gt; expects five arguments, four are supplied. Every invocation of this method throws &lt;code dir=&quot;auto&quot;&gt;IllegalFormatException&lt;/code&gt;. The Crawler appears to be a documentation generator that has not been run in a long time — the API it scrapes for has changed, but the generator has not been touched to follow. The defect is invisible because the tool is no longer used.&lt;/p&gt;
&lt;p&gt;Both findings are real defects. Both are essentially dead code (one on a rare error path, one in an unused tool). Their classification as Error is technically correct and operationally misleading.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-classification-actually-looks-like&quot;&gt;What the classification actually looks like&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The total: 19 findings classified as &lt;strong&gt;Error&lt;/strong&gt;. After triage:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Category&lt;/th&gt;&lt;th&gt;Count&lt;/th&gt;&lt;th&gt;Share&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Dead-code paths with crash potential&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;21 %&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Architectural defects with dormant impact&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;42 %&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;False positives (structural blindness)&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;37 %&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Findings with observed user incidents&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0 %&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Zero observed user incidents in fifteen years across all nineteen findings.&lt;/p&gt;
&lt;p&gt;Meanwhile, in the layers below Error, CodeQL reports the following counts on the same codebase:&lt;/p&gt;


















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Rule&lt;/th&gt;&lt;th&gt;Findings&lt;/th&gt;&lt;th&gt;Classified&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Missing catch of NumberFormatException&lt;/td&gt;&lt;td&gt;53&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Dereferenced variable may be null&lt;/td&gt;&lt;td&gt;38&lt;/td&gt;&lt;td&gt;Warning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Potential input resource leak&lt;/td&gt;&lt;td&gt;24&lt;/td&gt;&lt;td&gt;Warning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ignored error status of call&lt;/td&gt;&lt;td&gt;23&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Implicit conversion from array to string&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Potential output resource leak&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;Warning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Use of default toString()&lt;/td&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Non-synchronized override of synchronized method&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;(below visible threshold)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Fifty-three places where parsing a user-provided number can crash the application. Classified &lt;code dir=&quot;auto&quot;&gt;Note&lt;/code&gt;. Almost invisible.&lt;/p&gt;
&lt;p&gt;Twenty-four places where a file or network resource may not be closed, leading to file descriptor exhaustion on a long-running server. Classified &lt;code dir=&quot;auto&quot;&gt;Warning&lt;/code&gt;. Below &lt;code dir=&quot;auto&quot;&gt;Error&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Two places where overriding a &lt;code dir=&quot;auto&quot;&gt;synchronized&lt;/code&gt; method without re-applying &lt;code dir=&quot;auto&quot;&gt;synchronized&lt;/code&gt; creates a race condition in multi-threaded code. Buried so deep it does not even appear in the default UI summary.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-the-severity-system-breaks-down&quot;&gt;Where the severity system breaks down&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The pattern across the data is unambiguous. CodeQL’s &lt;code dir=&quot;auto&quot;&gt;Error&lt;/code&gt; tier is dominated by structural patterns that may produce defects in extreme cases but routinely do not. The &lt;code dir=&quot;auto&quot;&gt;Warning&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;Note&lt;/code&gt; tiers contain patterns that frequently produce real production incidents on real systems. &lt;strong&gt;The mapping from classification to operational urgency is inverted.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The reasons appear to be structural to how the tool reasons:&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M12 16a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm10.67 1.47-8.05-14a3 3 0 0 0-5.24 0l-8 14A3 3 0 0 0 3.94 22h16.12a3 3 0 0 0 2.61-4.53Zm-1.73 2a1 1 0 0 1-.88.51H3.94a1 1 0 0 1-.88-.51 1 1 0 0 1 0-1l8-14a1 1 0 0 1 1.78 0l8.05 14a1 1 0 0 1 .05 1.02v-.02ZM12 8a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V9a1 1 0 0 0-1-1Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Definite vs probable&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;CodeQL favors definite analytical conclusions (“this is always null”, “this assigns to itself”) over probabilistic ones (“this might be null”). Definite conclusions get Error. Probabilistic ones get Warning or Note. But probable failures on 53 lines of code matter more than definite failures on 2 dormant lines.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Pattern-level severity&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Every instance of a rule inherits the rule’s severity. There is no per-instance reasoning. A self-assignment that is dead code and a self-assignment that breaks a hot path receive the same Error tier. The reviewer cannot distinguish them from the severity column.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Cosmetic and structural together&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Rules like “Container contents are never accessed” cover three very different real-world cases: dead variables, dynamically accessed structures, and security-critical filters that fail open. All three receive Maintainability — Error. The CISO reviewing the report cannot find the security one without reading every finding.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3.06a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;No cross-rule prioritization&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A self-assignment with no impact is Error. A &lt;code dir=&quot;auto&quot;&gt;NumberFormatException&lt;/code&gt; not caught on user input is Note. There is no mechanism in CodeQL to bring the second above the first in any output. The order of rules in the output is alphabetic or chronological, never by operational risk.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;The combined effect is that a developer who opens CodeQL on a mature codebase sees a few dozen “Errors” — most of them irrelevant — and a few hundred “Notes” — among which the genuinely concerning ones are buried. The natural response, after the third or fourth review, is to stop opening CodeQL.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-codeql-actually-catches-well&quot;&gt;What CodeQL actually catches well&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This post would be unfair if it described only the failure modes. CodeQL does some things well, and the things it does well are worth naming.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Definite null dereference&lt;/strong&gt; (when the analyzer is sure, not “may be”). The &lt;code dir=&quot;auto&quot;&gt;ButtonGenCommand&lt;/code&gt; finding is a real crash trap. The rule has high precision.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Type-incompatible container modification.&lt;/strong&gt; The &lt;code dir=&quot;auto&quot;&gt;HotkeyManager&lt;/code&gt; finding is a real bug that compiles cleanly and silently misbehaves at runtime. Without static analysis, this kind of generic-erasure pitfall is genuinely hard to catch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Format string desync.&lt;/strong&gt; Both &lt;code dir=&quot;auto&quot;&gt;Missing format argument&lt;/code&gt; findings are real. The rule has very high precision because the analysis is local.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Self-assignment.&lt;/strong&gt; Both findings are real. The rule has perfect precision because there is no plausible legitimate use.&lt;/p&gt;
&lt;p&gt;What unites these is that they are &lt;strong&gt;narrow, local, and high-confidence&lt;/strong&gt; rules. CodeQL is at its best when the pattern is sharply defined and the analysis is bounded. It is at its worst when it generalizes — across rules, across severities, across code paths it cannot follow.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-we-configured-codeql-to-be-useful&quot;&gt;How we configured CodeQL to be useful&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;After triage, the actions we took for this scan:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Suppressed the Jython-related rules entirely&lt;/strong&gt;. &lt;code dir=&quot;auto&quot;&gt;Non-callable called&lt;/code&gt; and a few other Python rules produce structural false positives on every Jython-bridged file. There is no fix on our side. Suppressing the rule per-file via comments is more cost than benefit. We disable the rule in &lt;code dir=&quot;auto&quot;&gt;.codeql.yml&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Filed an internal issue for &lt;code dir=&quot;auto&quot;&gt;SikulixServer.allowedIPs&lt;/code&gt;&lt;/strong&gt; to investigate whether the network server is reachable and to either fix the missing filter or remove the server. The finding sat under Maintainability — Error, where nobody who triages security findings would look for it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Treated the 53 &lt;code dir=&quot;auto&quot;&gt;NumberFormatException&lt;/code&gt; findings as a focused remediation backlog&lt;/strong&gt;, separately from the Error-tier findings. They are not the same kind of work and should not be in the same queue.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Documented our dismissals&lt;/strong&gt;. For every Error-tier finding we dismissed, we wrote a one-line justification in the commit that closed the alert. This is the documentation our future selves will need when CodeQL re-flags the same finding after a re-scan.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Did not aim for zero Errors.&lt;/strong&gt; Zero CodeQL Errors is not a meaningful goal. A meaningful goal is zero unjustified Errors and a tracked, finite list of structural issues being worked on.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;why-we-still-keep-codeql-on&quot;&gt;Why we still keep CodeQL on&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Despite all of the above, CodeQL stays enabled on OculiX. The OpenSSF Scorecard checks for a SAST tool integration and rewards projects that have one. Procurement teams in regulated sectors check that CodeQL or an equivalent is configured. The signal value to outside observers is real, even when the operational value is poor.&lt;/p&gt;
&lt;p&gt;But the cost of running CodeQL is also real: the triage time, the alert fatigue, the temptation to ignore the tool entirely. We pay that cost in exchange for the external signal. We do not pretend that what we get back internally is anywhere near worth the marketing of static analysis.&lt;/p&gt;
&lt;p&gt;If you maintain a similar codebase and you are considering whether to enable CodeQL: enable it, run it once, do the triage we just walked through, and then decide rule-by-rule which queries you actually want to run. The default query suite assumes you have time to read several hundred findings. Most teams do not. The most useful configuration is the one that leaves you with twenty findings a week, all of which are worth opening.&lt;/p&gt;
&lt;p&gt;The boy who cried Error eventually had a wolf. We will probably see one, eventually, in a CodeQL scan. We will recognize it because it will be the only finding in months that we cannot dismiss in a single line. Everything else will continue to be the noise that the wolf is hidden in.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Repository: &lt;a href=&quot;https://github.com/oculix-org/Oculix&quot;&gt;github.com/oculix-org/Oculix&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The findings discussed are from a CodeQL scan on commit &lt;code dir=&quot;auto&quot;&gt;29a14d9&lt;/code&gt;. Detailed dismissal justifications are visible in the Security tab.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>codeql</category><category>static-analysis</category><category>code-quality</category></item><item><title>Why automation tools cannot type Chinese, and what OculiX does instead</title><link>https://oculix.org/fr/blog/2026-05-20-typing-chinese-japanese-clipboard-route/</link><guid isPermaLink="true">https://oculix.org/fr/blog/2026-05-20-typing-chinese-japanese-clipboard-route/</guid><description>An OculiX user filed a bug. Typing two Chinese characters into an input field silently produced garbage. The fix took fifteen lines of Java, but the explanation reaches back to how AWT was designed thirty years ago around the assumption that one character equals one keystroke. Here is what happens when that assumption meets the rest of the world.

</description><pubDate>Wed, 20 May 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In May 2026, an OculiX user filed &lt;a href=&quot;https://github.com/oculix-org/Oculix/issues/232&quot;&gt;issue #232&lt;/a&gt;. The summary was three lines long. The user wanted to call &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; with two Chinese characters in their automation script. Instead of typing them, the call produced silence, garbage, or in some IDE configurations a sequence of unrelated Latin characters depending on the active keyboard layout.&lt;/p&gt;
&lt;p&gt;The fix, when it landed, was fifteen lines of Java. The explanation behind it stretches back to how &lt;code dir=&quot;auto&quot;&gt;java.awt.Robot&lt;/code&gt; was designed in 1998, around an implicit assumption that one character on a screen corresponds to one keystroke on a keyboard. That assumption holds beautifully for ASCII Latin. It breaks for Chinese, Japanese, Korean, Arabic, Hindi, and even for accented Latin characters when the user’s keyboard layout does not happen to expose them as direct keys.&lt;/p&gt;
&lt;p&gt;This is the story of why visual automation frameworks have historically struggled to type non-ASCII, what OculiX decided to do about it, and why the workaround is more philosophically interesting than the bug it solves.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-bug-report&quot;&gt;The bug report&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The user, an active OculiX adopter, was automating an internal Chinese-language application. Their script needed to fill a search field with a person’s name. The straightforward call returned silently. The search field stayed empty. No exception was thrown. No log line surfaced anything unusual. The automation simply continued, the next step failed because the search field had no input, and the test reported FindFailed on whatever element was supposed to appear after the search.&lt;/p&gt;
&lt;p&gt;This is the worst kind of automation bug. There is no signal that something went wrong at the moment something went wrong. The error appears three steps later, attached to an unrelated element, with no link back to the actual failure point.&lt;/p&gt;
&lt;p&gt;After some investigation, the user established that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ASCII input worked: &lt;code dir=&quot;auto&quot;&gt;type(&quot;hello&quot;)&lt;/code&gt; produced “hello” in the field.&lt;/li&gt;
&lt;li&gt;French accented characters partly worked: typing a word with an accent produced the unaccented version on a US layout.&lt;/li&gt;
&lt;li&gt;Chinese input failed silently on every layout tested.&lt;/li&gt;
&lt;li&gt;Switching the OS keyboard layout to a Chinese IME did not help: the OculiX &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; call still produced garbage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The bug was filed not as a feature request but as a question: “Is this expected? How is everyone else handling CJK?”&lt;/p&gt;
&lt;p&gt;The honest answer was that everyone else was working around it. Manually. Each script that needed to type Chinese was implementing its own custom paste routine inline, copying the string to the clipboard with &lt;code dir=&quot;auto&quot;&gt;Jython&lt;/code&gt;’s &lt;code dir=&quot;auto&quot;&gt;java.awt.Toolkit&lt;/code&gt; calls and then sending &lt;code dir=&quot;auto&quot;&gt;Ctrl-V&lt;/code&gt; via &lt;code dir=&quot;auto&quot;&gt;keyDown&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;keyUp&lt;/code&gt;. The workaround was well-known among users who automated CJK applications, but it was not in the documentation, and it required understanding why the straightforward call did not work.&lt;/p&gt;
&lt;p&gt;This blog post explains the why, and what we did to make the straightforward call work.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-robotkeypress-fundamentally-cannot-type-chinese&quot;&gt;Why Robot.keyPress fundamentally cannot type Chinese&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;To understand why &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; with Chinese fails, you have to understand what &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; actually does under the hood. In OculiX (and in the original Sikuli, and in SikuliX), the call descends through several layers and eventually reaches &lt;code dir=&quot;auto&quot;&gt;java.awt.Robot.keyPress(int keycode)&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;java.awt.Robot.keyRelease(int keycode)&lt;/code&gt;, both members of the AWT API that has shipped with the Java platform since version 1.3 in May 2000.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Robot.keyPress(int keycode)&lt;/code&gt; is a thin wrapper around the operating system’s low-level keyboard event injection mechanism: &lt;code dir=&quot;auto&quot;&gt;SendInput&lt;/code&gt; on Windows, &lt;code dir=&quot;auto&quot;&gt;XTestFakeKeyEvent&lt;/code&gt; on X11/Linux, &lt;code dir=&quot;auto&quot;&gt;CGEventPost&lt;/code&gt; on macOS. All three of these accept a virtual key code, an integer identifier of a physical key on the keyboard. Not a character. Not a Unicode codepoint. A key.&lt;/p&gt;
&lt;aside aria-label=&quot;Virtual key codes are about keys, not characters&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Virtual key codes are about keys, not characters &lt;/p&gt;  &lt;div&gt;&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;KeyEvent.VK_A&lt;/code&gt; is the key labeled “A” on a US-QWERTY keyboard. On a French AZERTY keyboard, the same physical key is labeled “Q”. On a Dvorak keyboard, it is labeled “A” again but in a different position. The virtual key code is a hardware concept, divorced from the character it produces.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;This is the first fault line. When you call &lt;code dir=&quot;auto&quot;&gt;Robot.keyPress(KeyEvent.VK_A)&lt;/code&gt;, you are not asking the system to produce the character “A”. You are asking it to simulate pressing the physical key that, on the &lt;em&gt;currently active keyboard layout&lt;/em&gt;, would produce some character. The character depends on the layout. The layout depends on the user’s system configuration. The result depends on both.&lt;/p&gt;
&lt;p&gt;For pure ASCII Latin in a Western layout, the mapping is reasonably stable. The key &lt;code dir=&quot;auto&quot;&gt;VK_A&lt;/code&gt; produces “a” or “A” depending on shift state. The key &lt;code dir=&quot;auto&quot;&gt;VK_1&lt;/code&gt; produces “1” or ”!” depending on shift state. Sikuli historically maintained a hardcoded table mapping each ASCII character to a sequence of virtual key codes, and it worked because Western keyboards have a key for every ASCII character.&lt;/p&gt;
&lt;p&gt;The trouble starts the moment the character has no physical key on the keyboard.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-chinese-case&quot;&gt;The Chinese case&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;There is no constant for Chinese characters in &lt;code dir=&quot;auto&quot;&gt;java.awt.event.KeyEvent&lt;/code&gt;. There cannot be one. Chinese characters do not correspond to keys. A standard Chinese keyboard is, physically, a US-QWERTY keyboard. Chinese characters are produced through an &lt;strong&gt;Input Method Editor (IME)&lt;/strong&gt; that interprets sequences of QWERTY keystrokes as phonetic input (Pinyin), looks up matching characters, displays a candidate menu, and inserts the chosen character into the active text field once the user confirms a selection.&lt;/p&gt;
&lt;p&gt;To type a Chinese phrase through an IME, a Chinese user types Pinyin on the physical keyboard, the IME intercepts the sequence, recognizes it as the romanization of a candidate set, presents a candidate menu (because multiple character sequences share that romanization), and waits for the user to either accept the top candidate via space or pick a different one via number keys or arrow keys.&lt;/p&gt;
&lt;p&gt;This is not what &lt;code dir=&quot;auto&quot;&gt;Robot.keyPress&lt;/code&gt; does. &lt;code dir=&quot;auto&quot;&gt;Robot.keyPress&lt;/code&gt; injects events at a layer beneath the IME. The IME never sees them as Pinyin input. The events are interpreted as raw key events by the receiving application, which sees the Pinyin letters as literal characters. The Chinese intent is lost.&lt;/p&gt;
&lt;p&gt;Even more confusingly, on a system where the active keyboard layout is Chinese (with an IME enabled), &lt;code dir=&quot;auto&quot;&gt;Robot.keyPress(VK_N)&lt;/code&gt; might:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Be intercepted by the IME, treated as Pinyin input, and contribute to building a candidate menu that the user never sees because the automation does not pause to select&lt;/li&gt;
&lt;li&gt;Be passed through to the application as a literal “n” if the IME is in alphanumeric mode&lt;/li&gt;
&lt;li&gt;Be silently dropped if the IME is mid-composition for another sequence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The behavior is non-deterministic from the automation’s perspective, and entirely dependent on IME state, which the automation cannot reliably read or control through the AWT API.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-japanese-korean-arabic-hindi-cases&quot;&gt;The Japanese, Korean, Arabic, Hindi cases&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The same logic applies to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Japanese&lt;/strong&gt;, where IMEs interpret romaji or kana keystrokes into hiragana, katakana, or kanji&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Korean&lt;/strong&gt;, where IMEs assemble hangul syllables from jamo keystrokes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Arabic&lt;/strong&gt;, where the keyboard layout itself is non-Latin and Robot virtual keys do not map predictably&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hindi and other Indic scripts&lt;/strong&gt;, where IMEs combine consonants and vowel marks into syllabic clusters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vietnamese&lt;/strong&gt;, where IMEs add tonal diacritics through dedicated keys&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All these languages share the property that the displayed character is not a one-to-one mapping with a physical keystroke. The mapping is mediated by an IME that operates above the OS keyboard event layer that &lt;code dir=&quot;auto&quot;&gt;java.awt.Robot&lt;/code&gt; injects into.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-accented-latin-case-more-subtle&quot;&gt;The accented Latin case (more subtle)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Even within the Latin script, the same problem appears in a milder form. Consider typing a French word with an accent on three different keyboard layouts.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Layout&lt;/th&gt;&lt;th&gt;Result of typing a French word with accent&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;US-QWERTY&lt;/td&gt;&lt;td&gt;Accent silently dropped, no VK code maps to it&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;French AZERTY&lt;/td&gt;&lt;td&gt;Accent reproduced correctly&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;German QWERTZ&lt;/td&gt;&lt;td&gt;Accent dropped or garbage produced&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;UK-QWERTY&lt;/td&gt;&lt;td&gt;Accent dropped&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Spanish&lt;/td&gt;&lt;td&gt;Accent dropped (uses dead keys)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The accent characters have dead-key compositions on most layouts, which the AWT Robot cannot reliably simulate because dead keys are stateful at the OS layer and there is no portable way to query whether the dead key has been “consumed”.&lt;/p&gt;
&lt;p&gt;The same applies to many Latin extension characters. The character is “Latin”, but the keyboard layout determines whether it has a dedicated key or requires a composition sequence.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;summary-of-the-diagnosis&quot;&gt;Summary of the diagnosis&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The deep reason &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; with non-ASCII fails is not specific to OculiX or to Sikuli. It is a structural limitation of the &lt;code dir=&quot;auto&quot;&gt;java.awt.Robot&lt;/code&gt; API, which injects events at the keyboard event layer beneath the IME and beneath the keyboard layout translation. This API was designed in 1998 for testing GUI applications in an ASCII-Latin context, and it has never been extended to handle the layered character input model that the rest of the world uses.&lt;/p&gt;
&lt;aside aria-label=&quot;This is not a Java problem alone&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; This is not a Java problem alone &lt;/p&gt;  &lt;div&gt;&lt;p&gt;The same limitation applies to PyAutoGUI (Python: &lt;code dir=&quot;auto&quot;&gt;typewrite&lt;/code&gt; on CJK produces garbage), AutoHotkey on Windows (the &lt;code dir=&quot;auto&quot;&gt;Send&lt;/code&gt; command requires &lt;code dir=&quot;auto&quot;&gt;SendUnicode&lt;/code&gt; mode using a different injection path), Selenium with &lt;code dir=&quot;auto&quot;&gt;sendKeys&lt;/code&gt; (works only because the browser layer interprets the Unicode and dispatches it as DOM events, not as OS keystrokes), and most Robot Framework keyword libraries (each library handles or fails to handle CJK independently).&lt;/p&gt;&lt;p&gt;The visual automation ecosystem inherited this constraint from AWT. Solving it requires going around AWT, not through it.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;the-clipboard-route-as-workaround&quot;&gt;The clipboard route as workaround&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Once you accept that you cannot type CJK characters through the keystroke injection path, the question becomes: how do you get them into an input field at all? The most portable answer, used by professional automation engineers for two decades, is the clipboard.&lt;/p&gt;
&lt;p&gt;The idea is simple. Instead of pressing keys, you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy the target string to the system clipboard&lt;/li&gt;
&lt;li&gt;Send the OS-specific paste keystroke (&lt;code dir=&quot;auto&quot;&gt;Ctrl-V&lt;/code&gt; on Windows/Linux, &lt;code dir=&quot;auto&quot;&gt;Cmd-V&lt;/code&gt; on macOS)&lt;/li&gt;
&lt;li&gt;The receiving application processes the paste event and inserts the clipboard content into the focused field&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This route bypasses the keyboard layout layer entirely. The clipboard contains arbitrary Unicode bytes. The paste keystroke is a single key combination present on every modern platform. The receiving application sees the content as text, not as a sequence of keystrokes, and inserts it through its text handling code path, which is Unicode-aware.&lt;/p&gt;
&lt;p&gt;The clipboard route has been the unofficial standard workaround in the Sikuli community since at least 2012. It worked. But it required users to know about it, to implement it themselves, and to handle a list of edge cases (clipboard backup before pasting to avoid destroying user data, clipboard restoration after pasting, focus verification, etc.).&lt;/p&gt;
&lt;p&gt;The OculiX change for issue #232 was to make this route the default, transparently, for any input that contains characters outside the 7-bit ASCII range.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;implementation-fifteen-lines-of-java&quot;&gt;Implementation: fifteen lines of Java&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The actual change in OculiX is small enough to quote in full.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-detection&quot;&gt;The detection&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;containsNonAscii&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (text &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;charAt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;127&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Seven lines. We iterate over the input string and check whether any character has a codepoint greater than 127 (the 7-bit ASCII boundary). If yes, the input cannot be reliably typed through the keystroke path. If no, the input is pure ASCII Latin and can use the existing keystroke pipeline.&lt;/p&gt;
&lt;p&gt;The choice of 127 as the boundary is deliberate. Everything from 0 to 127 is ASCII, has a stable Robot mapping on Western keyboards, and works with the legacy code path. Everything above 127 is either Latin extension (accented characters), other scripts (CJK, Cyrillic, Arabic, etc.), or symbols. All of these benefit from going through paste rather than through simulated keystrokes.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-routing&quot;&gt;The routing&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;containsNonAscii&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;paste&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;keyin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null, text, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;FindFailed&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ex&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt; target, &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; throws FindFailed {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;containsNonAscii&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;paste&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;target, text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;keyin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;target, text, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The routing is two if-statements added before the existing logic. If the input contains non-ASCII characters, the call is forwarded to &lt;code dir=&quot;auto&quot;&gt;paste&lt;/code&gt; (which already exists and was already battle-tested). Otherwise, the existing &lt;code dir=&quot;auto&quot;&gt;keyin&lt;/code&gt; path runs unchanged.&lt;/p&gt;
&lt;p&gt;The two-argument variant &lt;code dir=&quot;auto&quot;&gt;type(target, text)&lt;/code&gt; first clicks the target image to focus the input, then routes the text through &lt;code dir=&quot;auto&quot;&gt;paste(target, text)&lt;/code&gt;, which performs the same click-then-paste sequence internally. The user-facing API is identical; only the internal routing changes.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;what-stays-on-keystrokes&quot;&gt;What stays on keystrokes&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt; target, &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; text, &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; modifiers&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;throws FindFailed {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;keyin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;target, text, modifiers&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The modifier-bearing variants &lt;code dir=&quot;auto&quot;&gt;type(text, modifiers)&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;type(target, text, modifiers)&lt;/code&gt; are intentionally &lt;strong&gt;not&lt;/strong&gt; routed through paste. Holding Shift, Ctrl, or Cmd while pasting Unicode characters has no meaningful semantic — these modifiers exist to shift the meaning of physical keystrokes (Shift+A produces “A”, Ctrl+S triggers a save action), not to alter clipboard content.&lt;/p&gt;
&lt;p&gt;If a user calls a modifier-bearing variant with CJK characters, the keystroke path runs and produces a FindFailed if the characters cannot be typed, which is the honest behavior. Silently switching to paste would lose the user’s intent.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-behavior-matrix&quot;&gt;The behavior matrix&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The result for users of the API is a clean four-way matrix:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Input&lt;/th&gt;&lt;th&gt;Modifiers&lt;/th&gt;&lt;th&gt;Path taken&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Pure ASCII&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;td&gt;Keystrokes&lt;/td&gt;&lt;td&gt;Backward compatible, fast, no clipboard side effect&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pure ASCII&lt;/td&gt;&lt;td&gt;Yes (Shift/Ctrl/Cmd)&lt;/td&gt;&lt;td&gt;Keystrokes&lt;/td&gt;&lt;td&gt;Modifiers map to keystroke semantics&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Non-ASCII&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;td&gt;Clipboard paste&lt;/td&gt;&lt;td&gt;Only way to reliably produce the characters&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Non-ASCII&lt;/td&gt;&lt;td&gt;Yes (Shift/Ctrl/Cmd)&lt;/td&gt;&lt;td&gt;Keystrokes (will likely fail)&lt;/td&gt;&lt;td&gt;Honest failure; modifier+paste has no meaning&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This is the kind of fallback that the user does not see and does not need to know about. The scripts that worked before still work the same. The scripts that previously needed manual clipboard workarounds now just work with the obvious call.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;tradeoffs-of-the-clipboard-route&quot;&gt;Tradeoffs of the clipboard route&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The clipboard path is not a free win. It has properties that are different from the keystroke path, and users automating sensitive workflows should be aware of them.&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M12 16a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm10.67 1.47-8.05-14a3 3 0 0 0-5.24 0l-8 14A3 3 0 0 0 3.94 22h16.12a3 3 0 0 0 2.61-4.53Zm-1.73 2a1 1 0 0 1-.88.51H3.94a1 1 0 0 1-.88-.51 1 1 0 0 1 0-1l8-14a1 1 0 0 1 1.78 0l8.05 14a1 1 0 0 1 .05 1.02v-.02ZM12 8a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V9a1 1 0 0 0-1-1Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Clipboard pollution&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Pasting destroys the previous clipboard content. The OculiX paste implementation backs up the clipboard before pasting and restores it afterward, but the backup-restore window is not instantaneous and can interfere with other clipboard listeners.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Focus dependency&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;The paste keystroke targets whatever has keyboard focus. A wrong paste is one chunk of misplaced text, much more visible than wrong keystrokes which usually masks the issue.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Paste timing&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Some applications throttle or queue paste events. Pasting twice in rapid succession can produce only one paste, or merge two pastes into one. The keystroke path is less susceptible.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M12 7a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1Zm0 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm9.71-7.44-5.27-5.27a1.05 1.05 0 0 0-.71-.29H8.27a1.05 1.05 0 0 0-.71.29L2.29 7.56a1.05 1.05 0 0 0-.29.71v7.46c.004.265.107.518.29.71l5.27 5.27c.192.183.445.286.71.29h7.46a1.05 1.05 0 0 0 .71-.29l5.27-5.27a1.05 1.05 0 0 0 .29-.71V8.27a1.05 1.05 0 0 0-.29-.71ZM20 15.31 15.31 20H8.69L4 15.31V8.69L8.69 4h6.62L20 8.69v6.62Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;No partial input&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A keystroke-based input can be interrupted in the middle. A paste is atomic. If you want autocomplete observation, the keystroke path is the right one.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;For most real-world automation use cases, these tradeoffs are acceptable. A QA engineer automating a Chinese banking form does not need character-by-character granularity; they need the form filled correctly. A test scenario filling Japanese customer data into a healthcare CRM does not depend on clipboard preservation across the operation.&lt;/p&gt;
&lt;p&gt;The cases where the tradeoffs matter — autocomplete observation, clipboard-watching tooling — are edge cases that the user can opt out of by explicitly calling &lt;code dir=&quot;auto&quot;&gt;keyin&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt;, or by providing the input one character at a time.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-enables-in-production&quot;&gt;What this enables in production&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Issue #232 was filed by a single user, but the impact of the fix is broader than that single use case. We have seen the change unlock automation work in several customer contexts since it landed.&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;APAC banking back-office&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A French bank with subsidiaries in Hong Kong and Singapore needed to automate regression tests on their Mandarin and Cantonese branch interfaces. Before the fix, their automation team maintained two separate codebases: one for European frontends, one for APAC with manual clipboard workarounds. The two codebases merged. Maintenance cost dropped by roughly a third.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3.06a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Japanese healthcare CRM&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A healthcare provider in Osaka automating patient intake workflows ran into the same wall. Patient names, addresses, prescription drug names — none survived the keystroke path. The team had a custom paste wrapper as workaround, but it predated the Ed25519 audit module and could not be signed for compliance. The integrated paste path now passes their audit.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M4.314 0.198L16.746 0.198Q17.838 0.198 18.615 0.975Q19.392 1.752 19.392 2.844L19.392 2.844L19.392 21.156Q19.392 22.248 18.615 23.025Q17.838 23.802 16.746 23.802L16.746 23.802L4.314 23.802Q3.222 23.802 2.445 23.025Q1.668 22.248 1.668 21.156L1.668 21.156L1.668 2.844Q1.668 1.752 2.445 0.975Q3.222 0.198 4.314 0.198L4.314 0.198ZM21.450 15.528L20.568 15.528L21.450 15.528Q21.786 15.528 22.038 15.759Q22.290 15.990 22.332 16.326L22.332 16.326L22.332 18.216Q22.332 18.552 22.122 18.783Q21.912 19.014 21.576 19.098L21.576 19.098L20.568 19.098L20.568 15.528L21.450 15.528ZM21.450 10.824L20.568 10.824L21.450 10.824Q21.786 10.824 22.038 11.034Q22.290 11.244 22.332 11.580L22.332 11.580L22.332 13.470Q22.332 13.806 22.122 14.058Q21.912 14.310 21.576 14.352L21.576 14.352L20.568 14.352L20.568 10.824L21.450 10.824ZM21.450 6.078L20.568 6.078L21.450 6.078Q21.786 6.078 22.038 6.309Q22.290 6.540 22.332 6.876L22.332 6.876L22.332 8.766Q22.332 9.102 22.122 9.333Q21.912 9.564 21.576 9.648L21.576 9.648L20.568 9.648L20.568 6.078L21.450 6.078ZM14.352 4.314L14.352 4.314L6.708 4.314Q6.372 4.314 6.120 4.545Q5.868 4.776 5.826 5.070L5.826 5.070L5.784 7.002Q5.784 7.296 6.015 7.548Q6.246 7.800 6.582 7.842L6.582 7.842L14.352 7.884Q14.688 7.884 14.940 7.653Q15.192 7.422 15.234 7.086L15.234 7.086L15.276 5.196Q15.276 4.818 15.003 4.566Q14.730 4.314 14.352 4.314Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;European multilingual e-commerce&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A French automation team running tests on a German marketplace was hitting silent failures whenever a product name contained German umlauts. The German keyboard layout was active on the test machine, but the keystroke table assumed French AZERTY and silently dropped the characters. The clipboard route bypasses all of this.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M8.7 10a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-6.27-6.3a1 1 0 0 0-1.42 1.42ZM21 14a1 1 0 0 0-1 1v3.59L15.44 14A1 1 0 0 0 14 15.44L18.59 20H15a1 1 0 0 0 0 2h6a1 1 0 0 0 .38-.08 1 1 0 0 0 .54-.54A1 1 0 0 0 22 21v-6a1 1 0 0 0-1-1Zm.92-11.38a1 1 0 0 0-.54-.54A1 1 0 0 0 21 2h-6a1 1 0 0 0 0 2h3.59L2.29 20.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L20 5.41V9a1 1 0 0 0 2 0V3a1 1 0 0 0-.08-.38Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Internal IT helpdesks across regions&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Multinational corporations run helpdesk automations interacting with internal ticketing systems containing employee names in dozens of native scripts. Cyrillic, Greek, Arabic, Hindi. Each required a separate workaround. The fix makes all of them automatic.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;The common thread is that internationalization in automation tooling has historically been an afterthought. Tools are built and tested in their authors’ language, and the assumption that “text input works” is rarely verified against the full Unicode space. Fixing this in OculiX brought a basic property back into line with what users reasonably expect.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-doesnt-solve&quot;&gt;What this doesn’t solve&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The fix is good but not complete. Several adjacent problems remain open.&lt;/p&gt;
&lt;aside aria-label=&quot;IME composition windows are still opaque&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; IME composition windows are still opaque &lt;/p&gt;  &lt;div&gt;&lt;p&gt;When a user types Chinese on a system with an IME enabled, the IME shows a small candidate window above the input field. OculiX cannot see this window through visual matching unless the user captures the candidate options as patterns. The paste path bypasses the IME entirely, so it does not produce candidate windows. This is fine when the goal is to insert known text. It does not help when the goal is to test the IME itself.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;aside aria-label=&quot;Characters outside the BMP need special handling&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Characters outside the BMP need special handling &lt;/p&gt;  &lt;div&gt;&lt;p&gt;The clipboard path handles characters in the Basic Multilingual Plane (codepoints 0 to 65535) cleanly, because Java strings represent these as single UTF-16 code units. Characters above 65535 (emojis, ancient scripts, mathematical symbols, some rare CJK extensions) are represented as surrogate pairs in Java strings. The current implementation handles these correctly by virtue of the clipboard being byte-oriented, but there is no explicit test coverage for surrogate pair edge cases. We track this as a known unknown.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;aside aria-label=&quot;Right-to-left scripts depend on the target application&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Right-to-left scripts depend on the target application &lt;/p&gt;  &lt;div&gt;&lt;p&gt;Pasting Arabic or Hebrew into an application that supports right-to-left rendering works as expected. Pasting the same into an application that does not produces visually corrupted output even though the underlying bytes are correct. This is a property of the target application, not of the automation tool.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;aside aria-label=&quot;Clipboard listeners may intercept the paste&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Clipboard listeners may intercept the paste &lt;/p&gt;  &lt;div&gt;&lt;p&gt;Some enterprise environments deploy clipboard monitoring tools that intercept all clipboard operations (data loss prevention software, compliance loggers, anti-exfiltration filters). These tools may log every paste OculiX performs, may block them entirely, or may modify the content in transit. Customers in regulated sectors should validate the interaction before deploying.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;the-structural-lesson&quot;&gt;The structural lesson&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The lesson behind this fix is not about Chinese or Japanese specifically. It is about what happens when a tool’s API and its underlying assumption diverge.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;type(String)&lt;/code&gt; reads, to a 2026 developer, like a Unicode-aware string-to-input function. The signature accepts a &lt;code dir=&quot;auto&quot;&gt;String&lt;/code&gt;, which in Java is fully Unicode-capable. The naive expectation is that whatever Unicode string you pass should appear in the target field, character by character, exactly as written.&lt;/p&gt;
&lt;p&gt;That expectation collides with an implementation that descends, through layers of well-meaning abstraction, into a 1998-era API that injects keyboard events at a layer beneath the keyboard layout. The mismatch between the modern API surface and the legacy implementation creates a silent failure mode that takes years of accumulated user reports to fully document.&lt;/p&gt;
&lt;p&gt;The fix is fifteen lines of Java because the workaround already existed in the codebase (the &lt;code dir=&quot;auto&quot;&gt;paste&lt;/code&gt; method). The change just added a smart router that picks the right path based on the input content. The hard part was not the code; it was identifying that the routing decision belonged at the &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; level, not at the user level.&lt;/p&gt;
&lt;p&gt;This is a recurring pattern in mature codebases. The capability is there. The pieces are in place. What is missing is the small connective decision that makes the capability available to users who do not know which internal method to call. A good API surface hides the choice between &lt;code dir=&quot;auto&quot;&gt;keyin&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;paste&lt;/code&gt;. The user calls &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; and gets the result they expected.&lt;/p&gt;
&lt;p&gt;For anyone maintaining a similar tool with a similar AWT-era foundation: this fix is generalizable. The same routing logic, the same ASCII boundary detection, the same paste fallback applies in PyAutoGUI, AutoHotkey, Robot Framework keyword libraries, or any other automation tool that relies on Robot-style keystroke injection. The 1998 API will not be redesigned. But its limitations can be bounded by a smarter layer above it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Repository: &lt;a href=&quot;https://github.com/oculix-org/Oculix&quot;&gt;github.com/oculix-org/Oculix&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Original issue: &lt;a href=&quot;https://github.com/oculix-org/Oculix/issues/232&quot;&gt;oculix-org/Oculix#232&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded><category>internationalization</category><category>java-awt</category><category>unicode</category></item><item><title>Storing spatial memory in a PNG: how we made visual UI matching 6.5× faster in OculiX</title><link>https://oculix.org/fr/blog/2026-05-19-png-metadata-visual-matching-speedup/</link><guid isPermaLink="true">https://oculix.org/fr/blog/2026-05-19-png-metadata-visual-matching-speedup/</guid><description>A weekend afternoon of measurement turned into a structural optimization for OculiX: persistent locator coordinates embedded in the PNG file itself, surviving git clone and cold-start JVMs. Measured ×6.5 on the find. Here is what we found, what we built, and what it changes for visual automation suites in CI.

</description><pubDate>Tue, 19 May 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is a long technical post about a small change. It explains a measurement we never thought to make, the discovery it produced, the implementation we built around it, and what it means for visual automation suites running in CI.&lt;/p&gt;
&lt;p&gt;The headline number is real but not particularly dramatic: visual UI matching in OculiX became roughly 6.5 times faster, measured on 50 cold-start JVM runs on an Intel i3 7th generation laptop. The interesting part is not the speedup itself. It is the path that led to it, and the structural lesson that came with it.&lt;/p&gt;
&lt;p&gt;If you maintain or use a visual automation framework, a test suite that watches pixels, or any tool that needs to find an image inside another image repeatedly across separate process invocations, the pattern we describe here will probably apply to you. The implementation took about 200 lines of Java, no external dependencies, and three micro-modifications inside the existing codebase. The standards we relied on have been published since 1996.&lt;/p&gt;
&lt;p&gt;What follows is the full story, ordered the way the investigation actually unfolded, not the way a marketing post would tell it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-benchmark-we-never-made&quot;&gt;The benchmark we never made&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;OculiX is the active continuation of the Sikuli and SikuliX visual automation lineage, MIT-licensed and used in production by close to 100 organizations across banking, defense, healthcare, manufacturing, and retail. Its core operation is a function called &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt;: given a small image (a button, an icon, a region of UI), find it inside the current screen capture and return its coordinates. Every other operation in the public API contains at least one call to &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; underneath.&lt;/p&gt;
&lt;aside aria-label=&quot;Why find matters so much&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Why find matters so much &lt;/p&gt;  &lt;div&gt;&lt;p&gt;Every public DSL method in OculiX (&lt;code dir=&quot;auto&quot;&gt;click&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;wait&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;exists&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;hover&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt;) calls &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; internally. The performance of &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; is therefore the upper bound on the performance of the entire suite. Optimizing it has a global effect, not a local one.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;Inside the codebase, &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; is well understood. It uses OpenCV template matching via JNI bindings, with five fallback strategies cascaded inside &lt;code dir=&quot;auto&quot;&gt;Finder.java&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.640 13.480L18.640 13.480Q19.560 13.480 20.220 12.840Q20.880 12.200 20.880 11.260Q20.880 10.320 20.220 9.660Q19.560 9 18.620 9Q17.680 9 17.020 9.660Q16.360 10.320 16.360 11.260Q16.360 12.200 17.020 12.840Q17.680 13.480 18.640 13.480ZM23.880 6.640L4.760 6.640L4.760 22.120L23.880 22.120L23.880 6.640ZM5.640 19L5.640 7.480L23 7.480L23 21L18.520 15.880L15.760 19.120L10.240 13.240L5.640 19ZM17.880 4L17.880 1.880L0.120 1.880L0.120 15.360L2.520 15.360L2.520 4L17.880 4Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Mode 1: Standard match&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Exact template matching with the configured similarity threshold. The fast path for stable UI elements.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Mode 2: DPI-aware rescale&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;If the screen DPI differs from the pattern capture DPI, the template is rescaled before matching.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M4.314 0.198L16.746 0.198Q17.838 0.198 18.615 0.975Q19.392 1.752 19.392 2.844L19.392 2.844L19.392 21.156Q19.392 22.248 18.615 23.025Q17.838 23.802 16.746 23.802L16.746 23.802L4.314 23.802Q3.222 23.802 2.445 23.025Q1.668 22.248 1.668 21.156L1.668 21.156L1.668 2.844Q1.668 1.752 2.445 0.975Q3.222 0.198 4.314 0.198L4.314 0.198ZM21.450 15.528L20.568 15.528L21.450 15.528Q21.786 15.528 22.038 15.759Q22.290 15.990 22.332 16.326L22.332 16.326L22.332 18.216Q22.332 18.552 22.122 18.783Q21.912 19.014 21.576 19.098L21.576 19.098L20.568 19.098L20.568 15.528L21.450 15.528ZM21.450 10.824L20.568 10.824L21.450 10.824Q21.786 10.824 22.038 11.034Q22.290 11.244 22.332 11.580L22.332 11.580L22.332 13.470Q22.332 13.806 22.122 14.058Q21.912 14.310 21.576 14.352L21.576 14.352L20.568 14.352L20.568 10.824L21.450 10.824ZM21.450 6.078L20.568 6.078L21.450 6.078Q21.786 6.078 22.038 6.309Q22.290 6.540 22.332 6.876L22.332 6.876L22.332 8.766Q22.332 9.102 22.122 9.333Q21.912 9.564 21.576 9.648L21.576 9.648L20.568 9.648L20.568 6.078L21.450 6.078ZM14.352 4.314L14.352 4.314L6.708 4.314Q6.372 4.314 6.120 4.545Q5.868 4.776 5.826 5.070L5.826 5.070L5.784 7.002Q5.784 7.296 6.015 7.548Q6.246 7.800 6.582 7.842L6.582 7.842L14.352 7.884Q14.688 7.884 14.940 7.653Q15.192 7.422 15.234 7.086L15.234 7.086L15.276 5.196Q15.276 4.818 15.003 4.566Q14.730 4.314 14.352 4.314Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Mode 3: Tolerant blur&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;GaussianBlur applied to both source and target. Tolerates antialiasing and subtle color variations.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Mode 4: Grayscale smart&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Conversion to grayscale before matching. Tolerates color theme changes.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M8.7 10a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-6.27-6.3a1 1 0 0 0-1.42 1.42ZM21 14a1 1 0 0 0-1 1v3.59L15.44 14A1 1 0 0 0 14 15.44L18.59 20H15a1 1 0 0 0 0 2h6a1 1 0 0 0 .38-.08 1 1 0 0 0 .54-.54A1 1 0 0 0 22 21v-6a1 1 0 0 0-1-1Zm.92-11.38a1 1 0 0 0-.54-.54A1 1 0 0 0 21 2h-6a1 1 0 0 0 0 2h3.59L2.29 20.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L20 5.41V9a1 1 0 0 0 2 0V3a1 1 0 0 0-.08-.38Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Mode 5: Multi-scale brute force&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Last resort. Tries multiple scales (0.5x to 2x) to catch significantly resized elements.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;The code is twenty years old at this point, refined incrementally by the original Sikuli authors at MIT in 2009, by Raimund Hocke from 2010 to 2025 under SikuliX, and now by the OculiX maintainers.&lt;/p&gt;
&lt;p&gt;Performance, in this kind of codebase, is rarely benchmarked from scratch. Everyone assumes it is whatever it has always been. A &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; call takes “some milliseconds”, or “a few hundred milliseconds” on a slow machine, and life goes on.&lt;/p&gt;
&lt;p&gt;So I sat down on a Sunday afternoon to actually measure it. Not because there was a problem. Because the question of how fast it really was had never been answered with a number on the current hardware I had in front of me.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-harness&quot;&gt;The harness&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The setup was deliberately simple. A standalone Java class called &lt;code dir=&quot;auto&quot;&gt;FindTiming&lt;/code&gt;, compiled directly against the OculiX complete-win jar, performing exactly one &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; per JVM invocation, then exiting. A batch script wrapping that class in a loop of fifty separate executions.&lt;/p&gt;
&lt;aside aria-label=&quot;Cold start matters&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Cold start matters &lt;/p&gt;  &lt;div&gt;&lt;p&gt;Most micro-benchmarks of OpenCV operations run inside a single JVM in a tight loop. This measures the algorithm after the JIT has warmed up, after libraries are loaded, after caches are populated. It produces optimistic numbers with no relationship to what users actually experience. Users run their test suites in CI, where each test typically corresponds to a fresh JVM. Any in-memory cache built during one invocation is lost before the next starts.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;So fifty cold starts. Each one paid the JVM startup cost, the OpenCV library load, the Tesseract OCR engine load, the OculiX framework initialization, then performed exactly one &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; and reported the elapsed time before exiting.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-baseline&quot;&gt;The baseline&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The result, on this five-year-old i3 laptop, was extremely consistent:&lt;/p&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Mean&lt;/td&gt;&lt;td&gt;&lt;strong&gt;502 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Median&lt;/td&gt;&lt;td&gt;502 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Minimum&lt;/td&gt;&lt;td&gt;480 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Maximum&lt;/td&gt;&lt;td&gt;545 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Range&lt;/td&gt;&lt;td&gt;65 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Standard deviation&lt;/td&gt;&lt;td&gt;18 ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Half a second to find a small image on a 1920 by 1080 screen. On a modern Intel i7 or Apple Silicon machine the number would be lower, probably by a factor of two or three. On a GitHub Actions standard runner it would be roughly comparable to the i3. On an older corporate desktop running a Citrix client through a VPN it would be slower again.&lt;/p&gt;
&lt;p&gt;Take 500 milliseconds and project it across a test suite:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Suite scale&lt;/th&gt;&lt;th&gt;Find calls&lt;/th&gt;&lt;th&gt;Pure find time @ 500ms&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Small functional suite&lt;/td&gt;&lt;td&gt;200&lt;/td&gt;&lt;td&gt;100 seconds&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Medium regression suite&lt;/td&gt;&lt;td&gt;1 000&lt;/td&gt;&lt;td&gt;8 min 20 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Large nightly suite&lt;/td&gt;&lt;td&gt;5 000&lt;/td&gt;&lt;td&gt;41 min 40 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Enterprise full coverage&lt;/td&gt;&lt;td&gt;20 000&lt;/td&gt;&lt;td&gt;2 h 46 min&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Multiply by the number of suites running per day in a CI environment, multiply by the number of CI minutes billed by the runner provider, and the cost becomes very real.&lt;/p&gt;
&lt;p&gt;That was the baseline. No optimization had been attempted yet. The number was simply the truth about what happened on the metal.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-was-already-in-the-code&quot;&gt;What was already in the code&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Before deciding what to optimize, the right move is always to look at what the code already does. OculiX inherits sixteen years of optimization attempts from the Sikuli and SikuliX lineage. A naive optimizer would re-read the OpenCV documentation and propose to switch to a faster matching algorithm. That would be a beginner mistake.&lt;/p&gt;
&lt;p&gt;The thing to investigate first is whether there is some optimization the existing code already attempts but cannot fully complete in the current configuration. That is almost always where the gains hide.&lt;/p&gt;
&lt;p&gt;A few minutes of grep revealed a symmetric pair of fields and a setting that nobody seems to talk about:&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3.06a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Image.lastSeen&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Private &lt;code dir=&quot;auto&quot;&gt;Rectangle&lt;/code&gt; field on every &lt;code dir=&quot;auto&quot;&gt;Image&lt;/code&gt; object. Stores the position of the most recent successful match. Paired with &lt;code dir=&quot;auto&quot;&gt;getLastSeen()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;setLastSeen(rect, score)&lt;/code&gt; accessors.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Settings.CheckLastSeen&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Public static boolean, set to &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; by default since at least 2018. Enables the optimization at the framework level.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;checkLastSeenAndCreateFinder&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Private method in &lt;code dir=&quot;auto&quot;&gt;Region.java&lt;/code&gt;. When called, creates a &lt;code dir=&quot;auto&quot;&gt;Finder&lt;/code&gt; restricted to the small rectangle around the previous match, falling back to full-screen only if needed.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;The intent of these three pieces is clear once you piece them together. After a successful &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt;, the rectangle of the match is stored in the &lt;code dir=&quot;auto&quot;&gt;Image&lt;/code&gt; object’s &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt; field. On the next call to &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; for the same image, if &lt;code dir=&quot;auto&quot;&gt;Settings.CheckLastSeen&lt;/code&gt; is true and &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt; is non-null, the code creates a &lt;code dir=&quot;auto&quot;&gt;Finder&lt;/code&gt; restricted to that small rectangle and tries to match there first. Only if the small-region match fails does it fall back to a full-screen scan.&lt;/p&gt;
&lt;p&gt;This is a classic spatial memoization pattern, well-known in computer vision. Sikuli implemented it correctly, a long time ago.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-pattern-works-until-it-doesnt&quot;&gt;The pattern works, until it doesn’t&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The optimization works beautifully inside a single JVM session. If you run a script that performs &lt;code dir=&quot;auto&quot;&gt;screen.find(&quot;submit_button.png&quot;)&lt;/code&gt; ten times in a row, the first call pays the full-screen scan cost, but the next nine calls find the image almost instantly through &lt;code dir=&quot;auto&quot;&gt;checkLastSeen&lt;/code&gt;. The cache hit rate is essentially 100 percent on stable UIs.&lt;/p&gt;
&lt;p&gt;There is, however, a subtle but critical limitation.&lt;/p&gt;
&lt;aside aria-label=&quot;The gap&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; The gap &lt;/p&gt;  &lt;div&gt;&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt; field lives in the in-memory representation of the &lt;code dir=&quot;auto&quot;&gt;Image&lt;/code&gt; object. It is a Java field. It exists as long as the JVM process exists, and not a millisecond longer. When the JVM exits, the &lt;code dir=&quot;auto&quot;&gt;Image&lt;/code&gt; object is garbage collected, the &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt; field disappears, and the next JVM that starts up begins from a blank slate.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;This is exactly what our benchmark exposed. Fifty separate JVM invocations, fifty &lt;code dir=&quot;auto&quot;&gt;Image&lt;/code&gt; objects with &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt; always equal to &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt; at the moment of the &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; call, fifty full-screen scans. The &lt;code dir=&quot;auto&quot;&gt;checkLastSeen&lt;/code&gt; optimization was active and present in the code throughout, but it had no input to work with. The cache was empty because the cache lived inside a process that died right after building it.&lt;/p&gt;
&lt;p&gt;This is the core observation. The existing optimization was correct in design. It was simply unable to bridge the gap between JVM invocations. In a typical test environment, where each test is its own process, the optimization never had a chance to engage.&lt;/p&gt;
&lt;p&gt;The code that solved the problem was already there, written long before this benchmark was performed, in a careful and well-tested form. The missing piece was not a clever algorithm. It was a way to keep the optimization’s input alive across process boundaries. A storage problem, not a computation problem.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-missing-piece-persistence&quot;&gt;The missing piece: persistence&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Once the gap was identified, the question became: how do you persist &lt;code dir=&quot;auto&quot;&gt;Image.lastSeen&lt;/code&gt; between JVM invocations? Several candidate approaches surfaced, each with their own trade-offs.&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Approach&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;th&gt;Drawbacks&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Sidecar file&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Write &lt;code dir=&quot;auto&quot;&gt;foo.png.position&lt;/code&gt; next to each PNG&lt;/td&gt;&lt;td&gt;Two files to commit, risk of desynchronization, clutter&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Central project file&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Single &lt;code dir=&quot;auto&quot;&gt;.oculix-positions.toml&lt;/code&gt; at project root&lt;/td&gt;&lt;td&gt;Linear lookup cost, merge conflicts in parallel CI, full file rewrite per change&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PNG ancillary chunk&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Embed the position metadata inside the PNG itself&lt;/td&gt;&lt;td&gt;Requires writing PNG-aware code, but standard since 1996&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The PNG ancillary chunk option won, for reasons that became clearer as we explored the constraint of CI environments.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;png-ancillary-chunks-the-underused-w3c-standard&quot;&gt;PNG ancillary chunks: the underused W3C standard&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The PNG file format, standardized by the W3C in 1996, is a structured container made of a fixed signature followed by a sequence of chunks. Each chunk has a four-byte length, a four-byte type, a variable-length data payload, and a four-byte CRC32 of the type and data.&lt;/p&gt;
&lt;p&gt;The PNG standard distinguishes between critical chunks (IHDR, IDAT, IEND, and others), which are mandatory for decoding the image, and ancillary chunks, which are optional and can be safely ignored by decoders that do not recognize them.&lt;/p&gt;
&lt;aside aria-label=&quot;PNG chunk casing convention&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; PNG chunk casing convention &lt;/p&gt;  &lt;div&gt;&lt;p&gt;A chunk type is four ASCII characters. The case of each character carries meaning:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;First character&lt;/strong&gt;: uppercase = critical (mandatory), lowercase = ancillary (optional)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Second character&lt;/strong&gt;: uppercase = public standard, lowercase = private&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third character&lt;/strong&gt;: reserved, must always be uppercase&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fourth character&lt;/strong&gt;: lowercase = safe to copy when editing, uppercase = strip on unrecognized&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This 30-year-old convention is respected by every serious image editor: Adobe, GIMP, Photoshop, Affinity, ImageMagick. Unknown ancillary chunks are preserved unless explicitly stripped.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;We chose the type code &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; for our chunk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lowercase &lt;code dir=&quot;auto&quot;&gt;o&lt;/code&gt;: ancillary, not critical&lt;/li&gt;
&lt;li&gt;Uppercase &lt;code dir=&quot;auto&quot;&gt;P&lt;/code&gt;: private to OculiX&lt;/li&gt;
&lt;li&gt;Uppercase &lt;code dir=&quot;auto&quot;&gt;L&lt;/code&gt;: reserved bit&lt;/li&gt;
&lt;li&gt;Lowercase &lt;code dir=&quot;auto&quot;&gt;x&lt;/code&gt;: safe to copy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Decoded: an optional, private, safe-to-copy chunk identified by &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt;. Other PNG tools encountering an OculiX-modified file see something they do not recognize, preserve it untouched on save, and ignore it on load. Compatibility is total.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-format-of-the-oplx-chunk&quot;&gt;The format of the oPLx chunk&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The internal layout of the &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk’s data payload is deliberately small. The goal was a fixed-size, fast-to-parse, easy-to-debug binary structure. The total payload is exactly 34 bytes:&lt;/p&gt;




































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Offset&lt;/th&gt;&lt;th&gt;Size&lt;/th&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Field&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;ASCII&lt;/td&gt;&lt;td&gt;Magic &lt;code dir=&quot;auto&quot;&gt;OPL\0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Redundant identifier, prevents misinterpretation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;uint16 (BE)&lt;/td&gt;&lt;td&gt;Version&lt;/td&gt;&lt;td&gt;Currently &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;. Bump on breaking format change.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;int32 (BE)&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;Pixel coordinate, last successful match&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;int32 (BE)&lt;/td&gt;&lt;td&gt;Y&lt;/td&gt;&lt;td&gt;Pixel coordinate, last successful match&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;14&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;int32 (BE)&lt;/td&gt;&lt;td&gt;Width&lt;/td&gt;&lt;td&gt;Match rectangle width, pixels&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;18&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;int32 (BE)&lt;/td&gt;&lt;td&gt;Height&lt;/td&gt;&lt;td&gt;Match rectangle height, pixels&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;22&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;int64 (BE)&lt;/td&gt;&lt;td&gt;Timestamp&lt;/td&gt;&lt;td&gt;UNIX epoch milliseconds, last update&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;30&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;int32 (BE)&lt;/td&gt;&lt;td&gt;Run count&lt;/td&gt;&lt;td&gt;Total successful matches since file creation&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Total: 34 bytes of payload, plus the standard 12 bytes of PNG chunk framing (4 bytes length, 4 bytes type, 4 bytes CRC32). Each pattern gains 46 bytes of metadata embedded in its PNG file. For a project with 500 patterns, this represents 23 kilobytes of additional space across the entire pattern library. Effectively negligible.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;design-choices-in-this-format&quot;&gt;Design choices in this format&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A few decisions deserve commentary.&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Big-endian byte order&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Non-negotiable. The PNG standard mandates big-endian for all multi-byte integers in chunk fields. Following the same convention inside our payload simplifies parsing and removes confusion with future tooling.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;32-bit signed for coordinates&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Generous. 16-bit unsigned would have sufficed for single-screen resolutions. We chose 32 bits to leave room for multi-monitor setups where coordinates extend into negative space and tens of thousands of pixels.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3.06a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;64-bit timestamp&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Standard UNIX epoch milliseconds. No bytes saved here. Audit trails that span years require room.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Run counter&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Allows detecting dead patterns (counter at zero), unstable patterns (high counter on young file), and locked-in patterns (counter grows without timestamp updates).&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;The chunk is plaintext at this stage. The integrity check is the standard CRC32 that PNG mandates at the end of every chunk; it catches accidental corruption but is not cryptographically strong.&lt;/p&gt;
&lt;aside aria-label=&quot;Future versions&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Future versions &lt;/p&gt;  &lt;div&gt;&lt;p&gt;A future version with the magic version field bumped will add an Ed25519 signature for environments that require tamper-evident position trails. For now, the chunk relies on the same trust model as the rest of the source code: git history, code review, and the integrity guarantees of the version control system.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;implementation-three-micro-modifications&quot;&gt;Implementation: three micro-modifications&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The actual change inside OculiX comes down to three modifications in two existing files, plus one new utility class. Total addition: roughly 250 lines of Java. No external dependencies. No new Maven coordinates. The standard JDK classes &lt;code dir=&quot;auto&quot;&gt;DataInputStream&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DataOutputStream&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;ByteBuffer&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;java.util.zip.CRC32&lt;/code&gt; cover all the needs.&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3.06a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;1. Image.load() reads the chunk&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;After &lt;code dir=&quot;auto&quot;&gt;ImageIO.read()&lt;/code&gt; decodes the pixels, a separate streaming read parses the PNG chunks, locates &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt;, and calls &lt;code dir=&quot;auto&quot;&gt;setLastSeen(rect, 1.0)&lt;/code&gt; on the current &lt;code dir=&quot;auto&quot;&gt;Image&lt;/code&gt; instance.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M8.7 10a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-6.27-6.3a1 1 0 0 0-1.42 1.42ZM21 14a1 1 0 0 0-1 1v3.59L15.44 14A1 1 0 0 0 14 15.44L18.59 20H15a1 1 0 0 0 0 2h6a1 1 0 0 0 .38-.08 1 1 0 0 0 .54-.54A1 1 0 0 0 22 21v-6a1 1 0 0 0-1-1Zm.92-11.38a1 1 0 0 0-.54-.54A1 1 0 0 0 21 2h-6a1 1 0 0 0 0 2h3.59L2.29 20.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L20 5.41V9a1 1 0 0 0 2 0V3a1 1 0 0 0-.08-.38Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;2. doCheckLastSeenAndCreateFinder expands the search&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Instead of creating a &lt;code dir=&quot;auto&quot;&gt;Region&lt;/code&gt; exactly the size of the previous match, it now creates a search box 2.5x larger, clamped to screen bounds. Tolerates UI drift between runs.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;3. find() writes the chunk after match&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;After updating in-memory &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt;, the chunk in the PNG file is streamed through a temp file and atomic-renamed. The position persists to disk.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;modification-1-imageload-reads-the-chunk&quot;&gt;Modification 1: Image.load() reads the chunk&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;Image.java&lt;/code&gt;, the existing &lt;code dir=&quot;auto&quot;&gt;load&lt;/code&gt; method reads the PNG file from disk into a &lt;code dir=&quot;auto&quot;&gt;BufferedImage&lt;/code&gt; using &lt;code dir=&quot;auto&quot;&gt;ImageIO.read&lt;/code&gt;. This call decodes only the image data (the &lt;code dir=&quot;auto&quot;&gt;IDAT&lt;/code&gt; chunks). It does not parse other chunks. Our addition opens the same file separately, in streaming mode, walks through its chunks until it finds the &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk if present, parses the position metadata, and calls &lt;code dir=&quot;auto&quot;&gt;setLastSeen(rect, 1.0)&lt;/code&gt; on the current &lt;code dir=&quot;auto&quot;&gt;Image&lt;/code&gt; instance.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In Image.java, after ImageIO.read(fileURL) at line 1018:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pngFile&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;fileURL&lt;/span&gt;&lt;span&gt;.toURI&lt;/span&gt;&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;chunk&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PngChunk&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;read&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pngFile, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;oPLx&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (chunk &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;chunk&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;34&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;ByteBuffer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ByteBuffer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;wrap&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;chunk&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;magic&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;magic&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (magic[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; magic[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; magic[&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;L&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; magic[&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getShort&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getInt&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getInt&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getInt&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getInt&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;setLastSeen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; Rectangle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cx, cy, cw, ch&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;1.0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ignored&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The streaming parser is deliberately optimized for the common case where the chunk is present and is one of the first non-critical chunks in the file. It skips the PNG signature (8 bytes), then enters a loop: read the chunk length, read the four-byte chunk type, compare it to &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt;, return the payload if matched, return null if &lt;code dir=&quot;auto&quot;&gt;IEND&lt;/code&gt; is reached, or skip the chunk data and CRC and continue otherwise.&lt;/p&gt;
&lt;aside aria-label=&quot;No bytes wasted&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; No bytes wasted &lt;/p&gt;  &lt;div&gt;&lt;p&gt;On a small PNG (a typical pattern of one to ten kilobytes), this parsing completes in well under one millisecond. On a large PNG, it remains under two milliseconds because the parser never reads the IDAT pixel data. It only walks the chunk headers.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;This is the architectural detail that matters most. The chunk reading is a strict addition that fills a gap in the existing system. It does not replace anything. It does not modify the contract of any existing method. If the chunk is absent (a legacy PNG that has never been processed by OculiX, a PNG whose chunk was stripped by an aggressive optimizer, a PNG generated by an external tool), the code falls through to &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt; being null, which is exactly the situation the existing codebase has handled for sixteen years. The fall-through path is the cold-start path. It still works. It is just slower.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;modification-2-dochecklastseenandcreatefinder-expands-the-search&quot;&gt;Modification 2: doCheckLastSeenAndCreateFinder expands the search&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;Region.java&lt;/code&gt;, the existing method creates a small &lt;code dir=&quot;auto&quot;&gt;Region&lt;/code&gt; exactly the size of the previous match rectangle. This works well for in-session use, where the image has just been matched at the exact same position. It works less well for cross-process use, where the UI may have drifted slightly between runs.&lt;/p&gt;
&lt;p&gt;Our modification expands the search rectangle by a factor of 2.5 around the stored center, clamped to the screen bounds.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Replace at line 2891:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Rectangle&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;img&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLastSeen&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;width&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2.5&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2.5&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (sw &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;) sw &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (sh &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) sh &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;width&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;y&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cx &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; sw &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cy &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; sh &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Translate to stay inside screen, do not truncate&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (sx &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) sx &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (sy &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) sy &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (sx &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; sw &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;) sx &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; sw;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (sy &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; sh &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) sy &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;screen&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; sh;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Region&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Region&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sx, sy, sw, sh&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The 2.5 multiplier was chosen empirically:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Multiplier&lt;/th&gt;&lt;th&gt;Effect&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;1.5x&lt;/td&gt;&lt;td&gt;Occasionally misses drifted patterns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2.0x&lt;/td&gt;&lt;td&gt;Marginal improvement, still occasional misses&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;2.5x&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Sweet spot: tolerates drift without ambiguity&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3.0x&lt;/td&gt;&lt;td&gt;No safety improvement, starts introducing ambiguity on dense UIs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;4.0x&lt;/td&gt;&lt;td&gt;Multiple visually similar elements get reached, confusing the matcher&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The clamping logic preserves the search box size when the pattern is near a screen edge. A pattern at (0, 1022) still gets a full 250 by 145 search box, just positioned at (0, 935) instead of being centered. The match rectangle for the original pattern still fits inside the search box.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;modification-3-find-writes-the-chunk-after-a-successful-match&quot;&gt;Modification 3: find() writes the chunk after a successful match&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code dir=&quot;auto&quot;&gt;Region.java&lt;/code&gt;, the existing &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; method already calls &lt;code dir=&quot;auto&quot;&gt;img.setLastSeen(lastMatch.getRect(), lastMatch.getScore())&lt;/code&gt; after a successful match. This updates the in-memory &lt;code dir=&quot;auto&quot;&gt;lastSeen&lt;/code&gt; field. Our addition extends this call with a write to the PNG file’s &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// At line 2284 of Region.find(), after setLastSeen:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;img&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setLastSeen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;lastMatch&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRect&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;lastMatch&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getScore&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// New: persist to PNG chunk&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pngFile&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;img&lt;/span&gt;&lt;span&gt;.getFileURL&lt;/span&gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.toURI&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ByteBuffer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ByteBuffer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;allocate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;34&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;O&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;L&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putShort&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;short&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Rectangle&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lastMatch&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRect&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putInt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putInt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;y&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putInt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;width&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putInt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putLong&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;currentTimeMillis&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putInt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;getRunCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;img&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;PngChunk&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pngFile, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;oPLx&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ignored&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The chunk-writing logic streams through the PNG file once, copying every chunk through to a temporary file, replacing the &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk in place if it already exists, or inserting a fresh &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk before the &lt;code dir=&quot;auto&quot;&gt;IEND&lt;/code&gt; marker if not. At the end, the temporary file replaces the original via an atomic file system rename.&lt;/p&gt;
&lt;p&gt;This streaming approach is more complex than reading the whole file into memory, modifying the byte array, and writing the result back, but it is meaningfully more robust:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Property&lt;/th&gt;&lt;th&gt;In-memory&lt;/th&gt;&lt;th&gt;Streaming (chosen)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Memory cost&lt;/td&gt;&lt;td&gt;Proportional to PNG size&lt;/td&gt;&lt;td&gt;Constant (~8 KB buffer)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Crash safety&lt;/td&gt;&lt;td&gt;Risk of partial write&lt;/td&gt;&lt;td&gt;Atomic rename: old or new, never half&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cost on small PNG&lt;/td&gt;&lt;td&gt;&amp;#x3C; 1 ms&lt;/td&gt;&lt;td&gt;1-2 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cost on large PNG (1 MB+)&lt;/td&gt;&lt;td&gt;20-50 ms + allocation&lt;/td&gt;&lt;td&gt;4-8 ms, no allocation spike&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;the-pngchunk-utility-class&quot;&gt;The PngChunk utility class&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Reading and writing PNG chunks does not require an external library. The format is simple enough that a 200-line utility class handles both operations with zero dependencies. The class exposes two methods:&lt;/p&gt;
&lt;starlight-tabs&gt; &lt;div&gt; &lt;ul role=&quot;tablist&quot;&gt; &lt;li role=&quot;presentation&quot;&gt; &lt;a role=&quot;tab&quot; href=&quot;#tab-panel-9&quot; id=&quot;tab-9&quot; aria-selected=&quot;true&quot; tabindex=&quot;0&quot;&gt;  Read API &lt;/a&gt; &lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt; &lt;a role=&quot;tab&quot; href=&quot;#tab-panel-10&quot; id=&quot;tab-10&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;  Write API &lt;/a&gt; &lt;/li&gt;&lt;li role=&quot;presentation&quot;&gt; &lt;a role=&quot;tab&quot; href=&quot;#tab-panel-11&quot; id=&quot;tab-11&quot; aria-selected=&quot;false&quot; tabindex=&quot;-1&quot;&gt;  Constants &lt;/a&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; &lt;div id=&quot;tab-panel-9&quot; aria-labelledby=&quot;tab-9&quot; role=&quot;tabpanel&quot;&gt; &lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;read&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt; png, &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; throws &lt;/span&gt;&lt;span&gt;IOException&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Returns the payload bytes if the chunk is found, &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt; otherwise. The read uses streaming &lt;code dir=&quot;auto&quot;&gt;DataInputStream&lt;/code&gt;, with &lt;code dir=&quot;auto&quot;&gt;skip()&lt;/code&gt; to bypass non-target chunks. Exits as soon as the target chunk is located.&lt;/p&gt; &lt;/div&gt;&lt;div id=&quot;tab-panel-10&quot; aria-labelledby=&quot;tab-10&quot; role=&quot;tabpanel&quot; hidden&gt; &lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt; png, &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; type, &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;[] payload&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; throws &lt;/span&gt;&lt;span&gt;IOException&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Streams the source PNG to a temp file, replacing or inserting the chunk, then atomic-renames the temp file over the original. CRC32 computed via &lt;code dir=&quot;auto&quot;&gt;java.util.zip.CRC32&lt;/code&gt; (hardware-accelerated on modern CPUs).&lt;/p&gt; &lt;/div&gt;&lt;div id=&quot;tab-panel-11&quot; aria-labelledby=&quot;tab-11&quot; role=&quot;tabpanel&quot; hidden&gt; &lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;SIGNATURE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;0x89&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;N&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;G&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;\r&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0x1a&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;IEND_BYTES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;I&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;N&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;D&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;The PNG file signature is 8 well-known bytes. The IEND chunk type terminates every PNG.&lt;/p&gt; &lt;/div&gt;  &lt;/starlight-tabs&gt;  
&lt;p&gt;The total code in &lt;code dir=&quot;auto&quot;&gt;PngChunk.java&lt;/code&gt; is 218 lines including comments, blank lines, and the class declaration. The file has no imports outside the &lt;code dir=&quot;auto&quot;&gt;java.io&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;java.nio&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;java.util.Arrays&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;java.util.zip.CRC32&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;java.nio.charset.StandardCharsets&lt;/code&gt; packages, all standard JDK.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;benchmark-methodology&quot;&gt;Benchmark methodology&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A benchmark is only useful if its methodology is described in enough detail that someone else can reproduce it.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;hardware-and-runtime&quot;&gt;Hardware and runtime&lt;/h3&gt;&lt;/div&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Item&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;CPU&lt;/td&gt;&lt;td&gt;Intel Core i3-7100 (2 cores, 4 threads, 3.9 GHz)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RAM&lt;/td&gt;&lt;td&gt;8 GB DDR4-2400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Storage&lt;/td&gt;&lt;td&gt;NVMe SSD&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OS&lt;/td&gt;&lt;td&gt;Windows 10 build 19045&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Java&lt;/td&gt;&lt;td&gt;OpenJDK 25 from Eclipse Temurin&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OculiX&lt;/td&gt;&lt;td&gt;3.0.3 release, feature branch with modifications&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Screen&lt;/td&gt;&lt;td&gt;1920 × 1080, no DPI scaling&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pattern&lt;/td&gt;&lt;td&gt;Windows search bar fragment, 12 × 58 pixels, at (0, 1022)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;harness-logic&quot;&gt;Harness logic&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Each benchmark run consisted of fifty independent JVM invocations, each:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Starting a fresh process&lt;/li&gt;
&lt;li&gt;Loading OculiX and its native dependencies&lt;/li&gt;
&lt;li&gt;Performing exactly one &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; call against the screen&lt;/li&gt;
&lt;li&gt;Printing the elapsed time in milliseconds to standard output&lt;/li&gt;
&lt;li&gt;Exiting&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The elapsed time was measured using &lt;code dir=&quot;auto&quot;&gt;System.nanoTime()&lt;/code&gt; immediately before and after the &lt;code dir=&quot;auto&quot;&gt;screen.find(pattern)&lt;/code&gt; call. This excludes JVM startup, library loading, and framework initialization. It includes only the find operation itself, including the screen capture inside the find.&lt;/p&gt;
&lt;p&gt;A separate post-processing class read the fifty timing lines from the captured log and computed: arithmetic mean, median, minimum, maximum, range, and standard deviation.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;two-scenarios-measured&quot;&gt;Two scenarios measured&lt;/h3&gt;&lt;/div&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scenario&lt;/th&gt;&lt;th&gt;Initial state of PNG&lt;/th&gt;&lt;th&gt;Expected behavior&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Baseline&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;No &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk&lt;/td&gt;&lt;td&gt;All 50 runs in full-screen scan&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Optimized&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk pre-written&lt;/td&gt;&lt;td&gt;All 50 runs in small-region scan&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Both scenarios were measured cold-start (50 separate JVM invocations) to ensure the comparison reflects the CI environment.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The numbers are the headline of this post. Let me state them precisely.&lt;/p&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Baseline (FULL)&lt;/th&gt;&lt;th&gt;Optimized (ROI)&lt;/th&gt;&lt;th&gt;Improvement&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;n&lt;/td&gt;&lt;td&gt;50&lt;/td&gt;&lt;td&gt;50&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;502 ms&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;77.7 ms&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;×6.46&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Median&lt;/td&gt;&lt;td&gt;502 ms&lt;/td&gt;&lt;td&gt;77 ms&lt;/td&gt;&lt;td&gt;×6.5&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Minimum&lt;/td&gt;&lt;td&gt;480 ms&lt;/td&gt;&lt;td&gt;63 ms&lt;/td&gt;&lt;td&gt;×7.6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Maximum&lt;/td&gt;&lt;td&gt;545 ms&lt;/td&gt;&lt;td&gt;113 ms&lt;/td&gt;&lt;td&gt;×4.8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Range&lt;/td&gt;&lt;td&gt;65 ms&lt;/td&gt;&lt;td&gt;50 ms&lt;/td&gt;&lt;td&gt;comparable&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Standard deviation&lt;/td&gt;&lt;td&gt;18 ms&lt;/td&gt;&lt;td&gt;11 ms&lt;/td&gt;&lt;td&gt;tighter&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Speedup factor on the find call alone: 6.46.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;observations-on-these-numbers&quot;&gt;Observations on these numbers&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Low variance in both conditions&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Standard deviation is 4% of mean in baseline, 14% in optimized. Neither is noisy enough to require repeated measurement.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M12 16a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm10.67 1.47-8.05-14a3 3 0 0 0-5.24 0l-8 14A3 3 0 0 0 3.94 22h16.12a3 3 0 0 0 2.61-4.53Zm-1.73 2a1 1 0 0 1-.88.51H3.94a1 1 0 0 1-.88-.51 1 1 0 0 1 0-1l8-14a1 1 0 0 1 1.78 0l8.05 14a1 1 0 0 1 .05 1.02v-.02ZM12 8a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V9a1 1 0 0 0-1-1Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Optimized scenario has a floor&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Roughly 60-70 ms cannot be reduced further by this technique. Dominated by screen capture (20-40 ms) and OpenCV setup (20-40 ms).&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Speedup depends on pattern size&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Small patterns (under 50×50 px) give the largest speedup. Large patterns (300×300+ px) give a smaller speedup because the 2.5x search region itself becomes substantial.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M1.44 8.855v-.001l3.527-3.516c.34-.344.802-.541 1.285-.548h6.649l.947-.947c3.07-3.07 6.207-3.072 7.62-2.868a1.821 1.821 0 0 1 1.557 1.557c.204 1.413.203 4.55-2.868 7.62l-.946.946v6.649a1.845 1.845 0 0 1-.549 1.286l-3.516 3.528a1.844 1.844 0 0 1-3.11-.944l-.858-4.275-4.52-4.52-2.31-.463-1.964-.394A1.847 1.847 0 0 1 .98 10.693a1.843 1.843 0 0 1 .46-1.838Zm5.379 2.017-3.873-.776L6.32 6.733h4.638l-4.14 4.14Zm8.403-5.655c2.459-2.46 4.856-2.463 5.89-2.33.134 1.035.13 3.432-2.329 5.891l-6.71 6.71-3.561-3.56 6.71-6.711Zm-1.318 15.837-.776-3.873 4.14-4.14v4.639l-3.364 3.374Z&quot; clip-rule=&quot;evenodd&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M9.318 18.345a.972.972 0 0 0-1.86-.561c-.482 1.435-1.687 2.204-2.934 2.619a8.22 8.22 0 0 1-1.23.302c.062-.365.157-.79.303-1.229.415-1.247 1.184-2.452 2.62-2.935a.971.971 0 1 0-.62-1.842c-.12.04-.236.084-.35.13-2.02.828-3.012 2.588-3.493 4.033a10.383 10.383 0 0 0-.51 2.845l-.001.016v.063c0 .536.434.972.97.972H2.24a7.21 7.21 0 0 0 .878-.065c.527-.063 1.248-.19 2.02-.447 1.445-.48 3.205-1.472 4.033-3.494a5.828 5.828 0 0 0 .147-.407Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Faster hardware preserves ratio&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;On modern CPUs, absolute timings shrink but the relative speedup factor stays at ×6-8. The overhead floor is proportionally less significant.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;hardware-projection&quot;&gt;Hardware projection&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If we extrapolate to other typical hardware:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Hardware&lt;/th&gt;&lt;th&gt;Baseline (FULL)&lt;/th&gt;&lt;th&gt;Optimized (ROI)&lt;/th&gt;&lt;th&gt;Speedup&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;i3 7th gen (measured)&lt;/td&gt;&lt;td&gt;502 ms&lt;/td&gt;&lt;td&gt;77 ms&lt;/td&gt;&lt;td&gt;×6.5&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;i7-13700K (projected)&lt;/td&gt;&lt;td&gt;~150-180 ms&lt;/td&gt;&lt;td&gt;~22-25 ms&lt;/td&gt;&lt;td&gt;×8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Apple M2/M3 (projected)&lt;/td&gt;&lt;td&gt;~80-120 ms&lt;/td&gt;&lt;td&gt;~10-15 ms&lt;/td&gt;&lt;td&gt;×8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GitHub Actions standard runner (projected)&lt;/td&gt;&lt;td&gt;~200-250 ms&lt;/td&gt;&lt;td&gt;~28-35 ms&lt;/td&gt;&lt;td&gt;×7&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-means-for-ci-suites&quot;&gt;What this means for CI suites&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The benchmark measures one &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; operation in isolation. A real-world test suite performs many operations, each containing at least one &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt;. Translating the per-operation speedup into a suite-level wall-clock improvement requires a few assumptions.&lt;/p&gt;
&lt;p&gt;Consider a moderately sized regression suite: 100 test cases, each performing 20 visual interactions on average, for &lt;strong&gt;2000 total &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; calls&lt;/strong&gt;.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scenario&lt;/th&gt;&lt;th&gt;Time per find&lt;/th&gt;&lt;th&gt;Total find time&lt;/th&gt;&lt;th&gt;Savings&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Baseline (i3)&lt;/td&gt;&lt;td&gt;500 ms&lt;/td&gt;&lt;td&gt;16 min 40 s&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Optimized (i3)&lt;/td&gt;&lt;td&gt;77 ms&lt;/td&gt;&lt;td&gt;2 min 34 s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;14 min 6 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Optimized (modern i7)&lt;/td&gt;&lt;td&gt;22 ms&lt;/td&gt;&lt;td&gt;44 seconds&lt;/td&gt;&lt;td&gt;&lt;strong&gt;15 min 56 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;economic-projection&quot;&gt;Economic projection&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If the suite runs once per pull request on a CI runner billed at $0.008 per minute (the rough GitHub Actions standard runner cost):&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Activity&lt;/th&gt;&lt;th&gt;Cost per run (baseline)&lt;/th&gt;&lt;th&gt;Cost per run (optimized)&lt;/th&gt;&lt;th&gt;Annual saving (50 PR/day, 250 days)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Suite execution&lt;/td&gt;&lt;td&gt;$0.13&lt;/td&gt;&lt;td&gt;$0.02&lt;/td&gt;&lt;td&gt;&lt;strong&gt;~$1 375 per suite&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;For organizations running multiple suites in parallel across many projects, the saving compounds quickly.&lt;/p&gt;
&lt;aside aria-label=&quot;The non-cost benefit&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; The non-cost benefit &lt;/p&gt;  &lt;div&gt;&lt;p&gt;The wall-clock improvement also changes how developers interact with the suite. A 17-minute suite that becomes a 3-minute suite invites local execution before push. A 17-minute loop discourages it. Team behavior shifts when the cost shifts.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;div&gt;&lt;h3 id=&quot;two-caveats&quot;&gt;Two caveats&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M12 16a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm10.67 1.47-8.05-14a3 3 0 0 0-5.24 0l-8 14A3 3 0 0 0 3.94 22h16.12a3 3 0 0 0 2.61-4.53Zm-1.73 2a1 1 0 0 1-.88.51H3.94a1 1 0 0 1-.88-.51 1 1 0 0 1 0-1l8-14a1 1 0 0 1 1.78 0l8.05 14a1 1 0 0 1 .05 1.02v-.02ZM12 8a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V9a1 1 0 0 0-1-1Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Find is not the only cost&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;The speedup applies to find time only. The full suite also contains network calls, server processing, browser rendering, mouse and keyboard events, and waits. A suite where finds dominate (visual regression, end-to-end smoke) sees larger wall-clock improvement than a suite waiting on slow back-ends.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Optimization rewards stability&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;Teams that regenerate patterns frequently see the chunk reset on each regeneration. The first run after regeneration pays the full-screen scan cost. The optimization rewards UI stability and frequent test execution — both characteristics of mature codebases.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;why-this-survives-git-clone-and-ci-runners&quot;&gt;Why this survives git clone and CI runners&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The structural argument for embedding the position in the PNG itself, rather than in a sidecar file or a central index, is best expressed by considering the CI lifecycle.&lt;/p&gt;
&lt;p&gt;A CI build typically starts from a clean state. A fresh container is provisioned, the repository is cloned from origin, dependencies are installed, the build runs, the tests run, the artifacts are collected, and the container is destroyed. There is no persistent state between builds. There is no shared file system. There is no cache that can be relied upon to be hot.&lt;/p&gt;
&lt;aside aria-label=&quot;The lifecycle problem&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; The lifecycle problem &lt;/p&gt;  &lt;div&gt;&lt;p&gt;In this environment, any cache that lives outside the cloned repository is wasted on the first build. A sidecar file at &lt;code dir=&quot;auto&quot;&gt;patterns/submit_button.png.position&lt;/code&gt; works only if it was committed to the repository. A central &lt;code dir=&quot;auto&quot;&gt;.oculix-positions.toml&lt;/code&gt; works only if it was committed. Both files require explicit author discipline to ensure they are committed alongside the PNG.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;oPLx&lt;/code&gt; chunk inside the PNG removes this discipline burden entirely. The chunk is part of the PNG file itself. When a developer runs the test suite locally and the chunk is updated, the modified PNG appears in &lt;code dir=&quot;auto&quot;&gt;git status&lt;/code&gt; automatically. Git tracks PNG files because they are committed in the project. The developer cannot commit “the update” without committing “the chunk inside the update”, because they are the same file.&lt;/p&gt;
&lt;p&gt;This means that when a CI build clones the repository, it gets the PNG file with the chunk already inside. The first test run in CI is already in the fast path. The optimization is active from the first second. No warm-up phase is needed. No cache must be primed. The position metadata is in the file, and the file is in the repository, and the repository is in the clone.&lt;/p&gt;
&lt;aside aria-label=&quot;The structural property that matters most&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; The structural property that matters most &lt;/p&gt;  &lt;div&gt;&lt;p&gt;This transforms a per-machine cache into a portable, version-controlled, distribution-friendly artifact. The optimization survives the most aggressive lifecycle: a brand new CI runner, a fresh clone, a one-time test execution, all benefit from a year of prior matches that were committed by a developer somewhere else.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;A team running the suite locally, committing the regenerated PNGs, and pushing to CI is effectively pre-warming the CI cache through normal development activity. There is no separate “cache warm-up” step. The cache warms itself as a side effect of using the tool.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-broader-pattern-png-chunks-as-runtime-context&quot;&gt;The broader pattern: PNG chunks as runtime context&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The technique of embedding metadata in image files is not new. Adobe XMP, EXIF, IPTC, GIMP layer information, and many other systems use this mechanism. What is new, or at least underused, is the embedding of runtime state — not authoring metadata, not provenance information, but actual operational data that evolves as the file is used.&lt;/p&gt;
&lt;p&gt;This pattern generalizes beyond visual automation:&lt;/p&gt;
&lt;div&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.640 13.480L18.640 13.480Q19.560 13.480 20.220 12.840Q20.880 12.200 20.880 11.260Q20.880 10.320 20.220 9.660Q19.560 9 18.620 9Q17.680 9 17.020 9.660Q16.360 10.320 16.360 11.260Q16.360 12.200 17.020 12.840Q17.680 13.480 18.640 13.480ZM23.880 6.640L4.760 6.640L4.760 22.120L23.880 22.120L23.880 6.640ZM5.640 19L5.640 7.480L23 7.480L23 21L18.520 15.880L15.760 19.120L10.240 13.240L5.640 19ZM17.880 4L17.880 1.880L0.120 1.880L0.120 15.360L2.520 15.360L2.520 4L17.880 4Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Texture caches for graphics applications&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A texture file could embed the time it was last loaded, the GPU memory pool it was allocated from, and the average frame time when it was active. A renderer could use this metadata to predict and pre-load textures.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Build artifacts for incremental compilation&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A compiled object file could embed the hash of the source it was compiled from, the compiler version, and the optimization level. An incremental build tool could detect when recompilation is actually necessary.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3.06a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Machine learning datasets&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;An image used in a vision model training set could embed its last classification result, the confidence score, and the model version. A cleaning tool could identify mislabeled samples without re-running the model.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;article&gt; &lt;p&gt; &lt;svg aria-hidden=&quot;true&quot; width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&gt;&lt;path d=&quot;M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span&gt;Audit logs for compliance&lt;/span&gt; &lt;/p&gt; &lt;div&gt;&lt;p&gt;A document image stored in a regulated workflow could embed a signed audit trail of the operations performed on it. The Ed25519 signing pattern from the OculiX MCP module would apply directly.&lt;/p&gt;&lt;/div&gt; &lt;/article&gt;&lt;/div&gt;
&lt;p&gt;The common thread across all these examples is the same: keep operational state with the artifact, in a format the artifact’s primary tools will preserve, so that the state survives every distribution mechanism the artifact ever encounters.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-doesnt-solve&quot;&gt;What this doesn’t solve&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A 6.5× speedup on the find is significant but not exhaustive. Several costs remain untouched by this optimization:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Cost component&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;th&gt;Approximate weight&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Screen capture&lt;/td&gt;&lt;td&gt;Unchanged&lt;/td&gt;&lt;td&gt;20-40 ms per find&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OpenCV setup&lt;/td&gt;&lt;td&gt;Unchanged&lt;/td&gt;&lt;td&gt;20-40 ms per find&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tesseract OCR loading&lt;/td&gt;&lt;td&gt;Unchanged&lt;/td&gt;&lt;td&gt;~300 ms per JVM cold start&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OculiX framework init&lt;/td&gt;&lt;td&gt;Unchanged&lt;/td&gt;&lt;td&gt;~1.5 seconds per JVM cold start&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;JVM startup itself&lt;/td&gt;&lt;td&gt;Unchanged&lt;/td&gt;&lt;td&gt;~1-2 seconds per cold start&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;aside aria-label=&quot;Honest framing&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Honest framing &lt;/p&gt;  &lt;div&gt;&lt;p&gt;This optimization eliminates a specific avoidable cost (the full-screen scan when a small-region scan would suffice) by closing a specific gap in the existing codebase (the inability to persist &lt;code dir=&quot;auto&quot;&gt;Image.lastSeen&lt;/code&gt; across JVM invocations). It does not promise that all other costs will collapse alongside.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;Reducing these other costs would require deeper structural changes: pre-built native images via GraalVM (the JVM startup), incremental OpenCV initialization (the OpenCV setup), lazy OCR loading (Tesseract), and possibly an alternative screen capture API on each platform. Each of these is a separate optimization project, none of which is in scope for the current change.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;closing-context-engineering-as-a-discipline&quot;&gt;Closing: context engineering as a discipline&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The lesson of this whole investigation is not “PNG chunks are cool” or “spatial memoization wins”. Both of those are true but uninteresting on their own.&lt;/p&gt;
&lt;p&gt;The lesson is structural.&lt;/p&gt;
&lt;aside aria-label=&quot;The general principle&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; The general principle &lt;/p&gt;  &lt;div&gt;&lt;p&gt;A large fraction of perceived “performance problems” in mature codebases are not algorithmic problems. They are problems of context that fails to cross a boundary. The boundary may be a process boundary (the JVM exits), a file system boundary (the cache is on the wrong machine), a network boundary (the data is in another datacenter), or a time boundary (yesterday’s state is gone today). The algorithm inside the boundary may be perfectly optimized, but the context that would let it shine never reaches it.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;p&gt;Finding these gaps requires a specific habit: benchmarking systems in the configuration users actually experience, not in the configuration that is convenient for developers. In a tight in-process loop, the OculiX &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; is fast. In a cold-start CI environment, it is slow. The same algorithm, the same code path, the same operating system. The only difference is whether the context built by the previous match has survived to inform the next match.&lt;/p&gt;
&lt;p&gt;The fix is rarely a new algorithm. It is usually a new place to store something that already existed, in a form that can travel through the boundaries the user’s actual workflow imposes.&lt;/p&gt;
&lt;p&gt;The PNG ancillary chunk happens to be a particularly elegant place to store visual automation context, because the artifact whose context we want to preserve is itself a PNG file, and the standard that defines PNG includes a mechanism specifically designed for this kind of metadata, and the tooling ecosystem has respected that mechanism for thirty years.&lt;/p&gt;
&lt;p&gt;The result is an optimization that did not require inventing a new algorithm, did not require breaking any existing contract, did not require any external dependency, and is fully backward compatible with files that do not yet have the chunk. It just needed someone to measure the actual baseline, identify the gap, and close it.&lt;/p&gt;
&lt;p&gt;For anyone maintaining a similar tool, my parting suggestion would be: go measure your cold-start performance against your warm-cache performance, in the same configuration your users actually run. If the gap is large, the optimization opportunity is probably not where you think it is. It is probably in the space between two of your existing components, in the form of a state that briefly exists and then disappears.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Repository: &lt;a href=&quot;https://github.com/oculix-org/Oculix&quot;&gt;github.com/oculix-org/Oculix&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Issue tracking the implementation of the persistent locator chunk: &lt;a href=&quot;https://github.com/oculix-org/Oculix/issues/353&quot;&gt;oculix-org/Oculix#353&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded><category>performance</category><category>visual-matching</category><category>benchmark</category></item><item><title>Refonte de DOMAutopsy en quatre heures, et les cinq choses que l&apos;IA n&apos;a pas pu décider</title><link>https://oculix.org/fr/blog/2026-05-18-rebuilding-domautopsy-in-four-hours/</link><guid isPermaLink="true">https://oculix.org/fr/blog/2026-05-18-rebuilding-domautopsy-in-four-hours/</guid><description>Une session Claude Code de quatre heures a transformé DOMAutopsy d&apos;un outil desktop en un runner web streamé. L&apos;IA a livré environ 4 000 lignes. Voici les cinq décisions d&apos;architecture qu&apos;elle ne pouvait pas prendre.

</description><pubDate>Mon, 18 May 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;J’ai passé un dimanche après-midi à refondre &lt;a href=&quot;https://github.com/julienmerconsulting/DOMAutopsy&quot;&gt;DOMAutopsy&lt;/a&gt;, le harvester de locators visuels que je maintiens, en passant d’un outil desktop PySide6 à un serveur FastAPI avec UI web en direct. Le travail a pris environ quatre heures. Claude Code a écrit la majorité des ~4 000 lignes livrées.&lt;/p&gt;
&lt;p&gt;Ce chiffre, seul, ne dit rien. C’est le genre de nombre que les gens citent sur LinkedIn pour vendre une prestation. Ce qu’il signifie réellement dépend entièrement des décisions qu’on laisse au modèle, et de celles qu’on garde pour soi.&lt;/p&gt;
&lt;p&gt;Cinq décisions me sont restées de cette session. Aucune n’était techniquement difficile. Toutes auraient produit un code sensiblement moins bon si j’avais accepté la première suggestion du modèle.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;1-le-système-navait-pas-besoin-de-rag&quot;&gt;1. Le système n’avait pas besoin de RAG&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;La première proposition de l’agent pour capturer le contexte sur une longue session de navigation était d’embedder les snapshots de page, de les stocker dans un index vectoriel, et de récupérer les top-k chunks quand le LLM avait besoin de raisonner sur la trajectoire. C’est une suggestion raisonnable en 2026. Elle aurait aussi ajouté une dépendance à une base vectorielle, à un modèle d’embeddings, et à un pipeline de retrieval que personne du côté utilisateur n’a demandé.&lt;/p&gt;
&lt;p&gt;Le vrai besoin était plus simple. L’agent observe le DOM en direct, le listener capture les sélecteurs, le rapport agrège tout à la fin. Il n’y a pas de problème de retrieval. Il y a un problème de context augmentation, et il se résout par un payload structuré passé au LLM à chaque étape.&lt;/p&gt;
&lt;p&gt;Première décision d’architecture : retirer toute une couche que le modèle aurait construite sans broncher.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;2-cdp-screencast-pas-novnc&quot;&gt;2. CDP screencast, pas noVNC&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Pour l’UI web en direct, deux options viables sont apparues : streamer l’instance Chromium headless via CDP screencast, ou l’envelopper dans un serveur VNC et la servir via noVNC. Les deux fonctionnent. Le modèle a recommandé noVNC parce que la littérature sur laquelle il est entraîné le présente comme le standard.&lt;/p&gt;
&lt;p&gt;En pratique, CDP screencast est plus léger, tourne dans le même process Playwright, et évite l’installation entière d’un serveur VNC dans l’image Docker. La voie noVNC aurait ajouté des pièces en mouvement dont personne n’a besoin une fois que Playwright expose déjà tout via CDP.&lt;/p&gt;
&lt;p&gt;C’est le genre de décision qui prend trente secondes de jugement et que le modèle n’a aucun moyen de prendre seul. Il optimise pour la réponse moyenne de son jeu d’entraînement, pas pour l’architecture que vous êtes en train de construire aujourd’hui.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;3-une-cascade-à-sept-niveaux-bat-un-matcher-llm-unique&quot;&gt;3. Une cascade à sept niveaux bat un matcher LLM unique&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Plusieurs projets open source des douze derniers mois proposent de déléguer la génération de sélecteurs entièrement à un LLM à chaque interaction. Le modèle traite le DOM, choisit le sélecteur le plus stable, retourne le résultat. Élégant sur le papier.&lt;/p&gt;
&lt;p&gt;DOMAutopsy utilise une approche différente : une cascade déterministe à sept niveaux dans un listener JavaScript de 372 lignes, le LLM n’intervenant qu’au nettoyage. Le tier 1 c’est &lt;code dir=&quot;auto&quot;&gt;data-testid&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;id&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;name&lt;/code&gt;. Le tier 7 c’est CSS court ou XPath texte. Le LLM ne voit jamais un sélecteur sauf si la cascade échoue ou produit des matchs ambigus.&lt;/p&gt;
&lt;p&gt;La raison est simple. Un matcher purement LLM est non-déterministe, coûteux à l’échelle, et crée une dépendance dure à un tiers à chaque interaction. Une cascade statique est auditable, rejouable hors-ligne, et bon marché. Le LLM apporte de la valeur uniquement là où la logique déterministe cesse d’être efficace.&lt;/p&gt;
&lt;p&gt;C’est une décision structurelle qu’aucun agent ne prendra à votre place, parce que tous les exemples qu’il a lus décrivent l’approche inverse.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;4-parser-regex-pas-parser-ast-pour-les-scripts-legacy&quot;&gt;4. Parser regex, pas parser AST, pour les scripts legacy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;DOMAutopsy peut faire du reverse engineering sur des scripts de test existants (Katalon, Playwright, Cypress, Selenium) pour les convertir en tâches structurées pour l’agent IA. La proposition du modèle était d’écrire un vrai parser AST par langage : Tree-sitter pour Groovy, ts-morph pour TypeScript, et ainsi de suite.&lt;/p&gt;
&lt;p&gt;Pour environ 95 pour cent des scripts de test réels en production, regex suffit. Ils sont linéaires, mécaniquement générés, répétitifs. Le parsing AST aurait multiplié le coût d’implémentation par dix et déplacé la surface d’échec de “la regex rate un cas de syntaxe” à “le parser AST crashe sur un fichier legacy malformé”.&lt;/p&gt;
&lt;p&gt;J’ai pris regex. J’ai aussi ajouté une note claire dans la documentation : ~95 pour cent de couverture, le reste passe en fallback manuel. Contrainte honnête, périmètre maîtrisable.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;5-pas-de-lock-in-saas-même-quand-ça-aurait-été-plus-rapide&quot;&gt;5. Pas de lock-in SaaS, même quand ça aurait été plus rapide&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;À un moment de la session, le modèle a proposé d’intégrer un service hébergé de diff de captures d’écran pour décharger le travail de comparaison visuelle. Cela aurait économisé un peu de temps d’implémentation.&lt;/p&gt;
&lt;p&gt;OculiX et DOMAutopsy vivent tous les deux dans des environnements régulés. Banques, contractants défense, providers santé. Ajouter une dépendance SaaS tierce sans décision consciente fermerait des portes que je devrais ensuite forcer pour les rouvrir. J’ai refusé, le modèle a accepté, le diff de captures se fait localement.&lt;/p&gt;
&lt;p&gt;C’est une décision non-technique habillée en décision technique. Le modèle ne sait pas qu’ajouter &lt;code dir=&quot;auto&quot;&gt;external-api.com&lt;/code&gt; au flux de données va doubler la longueur du dossier procurement pour un CISO en secteur régulé. Moi je le sais.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;ce-que-lia-fait-bien-et-ce-quelle-ne-fait-pas&quot;&gt;Ce que l’IA fait bien, et ce qu’elle ne fait pas&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;En quatre heures, l’IA a produit du code propre, fonctionnel, majoritairement idiomatique. Le débit est réel. Le résultat n’est pas magique. C’est le produit d’un prompting structuré, d’un modèle mental clair côté architecte, et d’un flux continu de petits jugements qui ferment les mauvaises branches avant qu’elles ne grossissent.&lt;/p&gt;
&lt;p&gt;La question intéressante n’est pas “l’IA peut-elle remplacer les architectes seniors”. C’est : quelles décisions allez-vous encore prendre vous-même, et lesquelles êtes-vous prêt à déléguer. Les cinq ci-dessus sont des décisions que je refuserais de déléguer même avec vingt ans d’expérience supplémentaires. Elles sont spécifiques au domaine, sensibles au contexte, et elles façonnent le reste du projet pendant des années.&lt;/p&gt;
&lt;p&gt;Le reste, l’IA l’a livré.&lt;/p&gt;
&lt;p&gt;—&lt;/p&gt;
&lt;p&gt;Repo : &lt;a href=&quot;https://github.com/julienmerconsulting/DOMAutopsy&quot;&gt;github.com/julienmerconsulting/DOMAutopsy&lt;/a&gt;&lt;/p&gt;</content:encoded><category>agentic-ai</category><category>qa-automation</category><category>claude-code</category></item></channel></rss>