<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://eric15342335.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://eric15342335.github.io/" rel="alternate" type="text/html" /><updated>2026-11-05T00:44:07+08:00</updated><id>https://eric15342335.github.io/feed.xml</id><title type="html">Cheng Ho Ming, Eric</title><subtitle>HKU BASc(AppliedAI) undergraduate with a second major in Computer Science.</subtitle><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><entry xml:lang="en"><title type="html">Naruto shadow clones invisible on iPad Safari</title><link href="https://eric15342335.github.io/blog/2026/11/04/naruto-clones-invisible-on-ipad-safari.html" rel="alternate" type="text/html" title="Naruto shadow clones invisible on iPad Safari" /><published>2026-11-04T21:45:00+08:00</published><updated>2026-11-04T21:45:00+08:00</updated><id>https://eric15342335.github.io/blog/2026/11/04/naruto-clones-invisible-on-ipad-safari</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/11/04/naruto-clones-invisible-on-ipad-safari.html"><![CDATA[<p><a href="https://github.com/Nocodile-AI/naruto-gesture-detection">naruto-gesture-detection</a> detects a hand sign in the browser and plays a Naruto shadow clone effect over the webcam. The effect comes from <a href="https://github.com/nasha-wanich/naruto-shadow-clone-jutsu">naruto-shadow-clone-jutsu</a> by Nasha Wanich. The same code works on Windows and Android. On my <a href="https://support.apple.com/en-us/111887">iPad Air (5th generation)</a>, <a href="https://support.apple.com/en-us/100100">iPadOS 27.0.1</a>, Safari, the clones never showed up.</p>

<p>The cause is <a href="https://bugs.webkit.org/show_bug.cgi?id=221542">WebKit bug 221542</a>, open since 2021: Apple’s WebKit ignores <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation"><code>globalCompositeOperation</code></a> when the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage"><code>drawImage</code></a> source is a <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement">video</a>. The <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/commit/b443c3e95538658ba9a2a0b6b5a7bc3901913106">fix</a> swaps the order of two draw calls.</p>

<p>I found the symptom, added a diagnostics panel with AI help, noticed that the test button and the real trigger behaved differently, directed the work and tested on the iPad. <a href="https://www.anthropic.com/news/claude-opus-5-5">Claude Opus 5.5</a> in <a href="https://www.anthropic.com/claude-code">Claude Code</a> traced the code, read the WebKit and MediaPipe source, ran the probes, and wrote the emulation, the fix and the test.</p>

<p>The first half of this post is the bug. The second half is about AI, because the way this bug got solved has bothered me more than the bug did.</p>

<h2 id="the-symptom">The symptom</h2>

<p>On the iPad:</p>

<ul>
  <li>Training works and the confidence score reaches 100%.</li>
  <li>The status says the clone sequence is active and the badge icon glows yellow.</li>
  <li>The blue hand-landmark dots disappear.</li>
  <li>No clones and no smoke appear. The camera view looks normal.</li>
  <li>The “Test clone effect” button in the <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/commit/12b0043e0d6f838d98fad0c61e32acf1a7e98619">Diagnostics panel</a> does show smoke and blue person figures.</li>
</ul>

<h2 id="why-ipad-only-bugs-are-expensive">Why iPad-only bugs are expensive</h2>

<p>When something works on Windows and Android and fails only on iPad, you know it is a platform-specific bug, but you cannot debug it the usual way from a Windows PC. To inspect a page on an iPad, <a href="https://webkit.org/web-inspector/enabling-web-inspector/">Web Inspector</a> has to run from Safari on a connected Mac. I could not do that from my Windows PC, so I had no console or element view for the page on the iPad. Playwright’s WebKit on Windows cannot show this bug either, for the reason given further down. The failure also produced no error: the app’s diagnostics panel records uncaught errors and rejected promises, and all 193 entries in the log in the next section are at info level. So my evidence was what I could see on the iPad and what the app logged. iPad-only bugs are expensive to solve, because they usually end in low-level rendering engine behaviour that you did not write.</p>

<p>Debugging it the traditional way takes a lot of background:</p>

<ul>
  <li>You need to read browser engine code, or at least know where to look. Here that means WebKit’s canvas code.</li>
  <li>You need to know that iPad browsers share an engine. Safari, Chrome, Firefox and Brave on iPad normally all run on WebKit, because <a href="https://developer.apple.com/app-store/review/guidelines/#2.5.6">App Review guideline 2.5.6</a> tells browsers to use it. If you miss this, the same failure in a second browser could convince you the engine is not the problem.</li>
  <li>You need the right search words. Claude’s search, further down, contains <code>drawImage</code>, <code>globalCompositeOperation</code> and <code>source-in</code>. You only know those words after you have narrowed down the cause, and from my experience Google will not return a WebKit bug report for a description of what you see on screen.</li>
</ul>

<p>The engine rule has regional exceptions. Apple lets browsers <a href="https://developer.apple.com/support/alternative-browser-engines/">in the EU</a> use another engine on iPadOS 18 and later, and its <a href="https://developer.apple.com/support/alternative-browser-engines-jp/">rules for Japan</a> allow it on iOS 26.2 and later but name only iOS for distribution. <a href="https://open-web-advocacy.org/blog/how_apples_key_tactic_could_prevent_japans_smartphone_act_from_improving_browser_competition/">Open Web Advocacy said in January 2026</a> that no browser vendor had shipped a competing engine, and I found no report of a public release since.</p>

<h2 id="the-log-shows-no-errors">The log shows no errors</h2>

<p>A diagnostics log from Safari (<code>ipad-diagnostics-2026-10-08.json</code>) shows this on a real trigger:</p>

<pre><code class="language-text">clone readiness {"assetsReady":true,"segmentationEnabled":true,"maskReady":true,"active":false}
clone started {"testMode":false}
canvas render-state {"stage":"clone-effect"}
canvas first-clones-rendered {"width":640,"height":480,"testMode":false}
canvas clone-render-skipped {"reason":"expired","elapsed":4624}
</code></pre>

<p>Assets loaded, the mask existed, the render ran for its 4.6 seconds, and nothing logged an error. So the draw calls ran, and their output could not be seen.</p>

<p>The log’s user agent says <code>Macintosh ... Version/26.5 Safari</code>. <a href="https://webkit.org/blog/9674/new-webkit-features-in-safari-13/">Safari on iPad sends a Mac user agent by default</a>, so the log alone cannot tell an iPad from a Mac. The log dates from minutes after my <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/commit/12b0043e0d6f838d98fad0c61e32acf1a7e98619">diagnostics commit</a>, on an older OS than the later 27.0.1 test.</p>

<h2 id="test-button-vs-real-trigger">Test button vs real trigger</h2>

<p>The test button works and the real trigger does not, even though they share the drawing code. I noticed that on the iPad and told Claude.</p>

<p>Claude traced the difference to the person layer, which goes on top of everything so the real person stands in front of the clones. The real trigger <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/blob/12b0043e0d6f838d98fad0c61e32acf1a7e98619/src/clone-effect.ts#L325-L327">builds it</a> from the <a href="https://github.com/google-ai-edge/mediapipe/blob/master/docs/solutions/selfie_segmentation.md">segmentation mask</a> and the camera video. The test button <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/blob/12b0043e0d6f838d98fad0c61e32acf1a7e98619/src/clone-effect.ts#L318-L323">paints its own see-through figure</a>. So the button passed because it skipped the broken step, which is a bit annoying given I added it to debug this.</p>

<h2 id="the-smoke-clue">The smoke clue</h2>

<p>Smoke images do not use the mask at all, yet they vanished too. The person layer is the only thing drawn after the smoke. So the working theory became that the person layer is an opaque full frame on iOS.</p>

<p>An opaque copy of the live video, drawn over the clones, sits exactly on top of the real video and looks like nothing happened. That also explains the missing blue dots: the effect really had started, and landmarks are not drawn during it.</p>

<p><a href="https://github.com/Nocodile-AI/naruto-gesture-detection/blob/12b0043e0d6f838d98fad0c61e32acf1a7e98619/src/clone-effect.ts#L325-L327">The old code</a>:</p>

<pre><code class="language-ts">ctx.drawImage(mask, 0, 0, width, height);
ctx.globalCompositeOperation = "source-in";
ctx.drawImage(video, 0, 0, width, height);
</code></pre>

<p>For that to come out opaque, either the mask is opaque or Safari ignores <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation#value"><code>source-in</code></a> for the video.</p>

<h2 id="the-wrong-theory-mediapipe">The wrong theory: MediaPipe</h2>

<p>The mask was the first suspect. <a href="https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation@0.1.1675465747/selfie_segmentation.js">MediaPipe’s bundle</a> has an iOS-only branch: <code>useCpuInference</code> defaults to true when <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform"><code>navigator.platform</code></a> is an iPad or iPhone, or when the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent">user agent</a> contains <code>Mac</code> and the device has touch. It looked like the answer, since it is a real difference between iPad and everything else.</p>

<p>So Claude measured it. A probe read actual mask pixels with the branch off and on, in Chromium and in real WebKit on Windows, including a spoofed iPad platform and a Mac user agent with touch. Every variant gave a correct mask: corner alpha 0 and similar coverage. The theory was plausible and wrong, and measuring it took minutes.</p>

<h2 id="the-webkit-source">The WebKit source</h2>

<p>That left the video. (The links in this section point at WebKit commit <a href="https://github.com/WebKit/WebKit/commit/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7"><code>74fe159</code></a>, which was <code>main</code> when I checked them.)</p>

<p>In <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp#L2264-L2265"><code>CanvasRenderingContext2DBase.cpp</code></a>, the Apple-only (<a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp#L2262"><code>USE(CG)</code></a>) branch of <code>drawImage</code> for video calls:</p>

<pre><code class="language-cpp">c-&gt;drawNativeImage(*image, normalizedDstRect, normalizedSrcRect);
</code></pre>

<p>No paint options are passed, so <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/platform/graphics/GraphicsContext.h#L263"><code>drawNativeImage</code></a> gets its empty default. The default in <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/platform/graphics/RasterDrawingOptions.h#L121"><code>RasterDrawingOptions.h</code></a> is <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/platform/graphics/GraphicsTypes.h#L43"><code>CompositeOperator::SourceOver</code></a>, and <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/platform/graphics/cg/GraphicsContextCG.cpp#L489"><code>GraphicsContextCG.cpp</code></a> applies the options’ mode. The <a href="https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap"><code>ImageBitmap</code></a> path <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp#L2323">does pass <code>state().globalComposite</code></a>.</p>

<p>Camera video needed its own check, because it comes from a <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStream"><code>MediaStream</code></a> player. <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaStreamAVFObjC.h"><code>MediaPlayerPrivateMediaStreamAVFObjC</code></a> has no <code>nativeImageForCurrentTime</code>, so it gets the <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/platform/graphics/MediaPlayerPrivate.h#L212">base class’s null</a>. <code>drawImage</code> then falls through to <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp#L2276"><code>paintCurrentFrameInContext</code></a>, and the <a href="https://github.com/WebKit/WebKit/blob/74fe1595a60a6d9e3df91a87aa6d3f5acb4c9cc7/Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaStreamAVFObjC.mm#L1125-L1147">player’s version</a> also calls <code>drawNativeImage</code> with default options. Both routes end at <code>SourceOver</code>.</p>

<h2 id="the-search-and-the-bug-report">The search and the bug report</h2>

<p>Once the suspects were clear, Claude ran this search:</p>

<pre><code class="language-text">Safari canvas drawImage video ignores globalCompositeOperation source-in WebKit bug
</code></pre>

<p>The top result was <a href="https://bugs.webkit.org/show_bug.cgi?id=221542">WebKit bug 221542</a>, “globalCompositeOperation has not effect on drawing a video to a canvas” (the typo is in the title). It dates from February 2021 and its status is still NEW. A patch got review approval but never landed, and <a href="https://bugs.webkit.org/show_bug.cgi?id=221542#c7">a commenter reproduced the bug on Safari 16.1</a> in October 2022.</p>

<h2 id="reproducing-it-without-a-mac">Reproducing it without a Mac</h2>

<p>Nothing in this work ran Apple’s renderer, so this section is an emulation of the bug. <a href="https://playwright.dev/docs/browsers">Playwright’s WebKit</a> on Windows does not compile the <code>USE(CG)</code> code, so it cannot show the bug. Linux WebKit in Docker has the same limit, so Claude skipped Docker. Running macOS in a container was not an option.</p>

<p>So Claude patched Chromium to behave the way the source says Safari behaves. <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/blob/b443c3e95538658ba9a2a0b6b5a7bc3901913106/tests/e2e/clone-compositing.spec.ts#L17-L33">The patch</a> wraps <code>drawImage</code> and forces <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation#value"><code>source-over</code></a> whenever the source is a video:</p>

<pre><code class="language-ts">CanvasRenderingContext2D.prototype.drawImage = function (...args) {
  if (!(args[0] instanceof HTMLVideoElement)) return drawImage.apply(this, args);
  const operation = this.globalCompositeOperation;
  this.globalCompositeOperation = "source-over";
  drawImage.apply(this, args);
  this.globalCompositeOperation = operation;
};
</code></pre>

<p>The real <code>CloneEffect</code> then ran with a real MediaPipe mask and real smoke images. The camera was a drawn test figure, so the mask is rough.</p>

<table>
  <thead>
    <tr>
      <th>Render</th>
      <th>Opaque canvas</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Normal browser, old code</td>
      <td>28.1%</td>
      <td>Clones and smoke</td>
    </tr>
    <tr>
      <td>Safari behavior, old code</td>
      <td>100%</td>
      <td>Plain video, same as the iPad</td>
    </tr>
    <tr>
      <td>Safari behavior, fixed code</td>
      <td>28.1%</td>
      <td>Clones and smoke</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/2026-11-04-naruto-clones-invisible-on-ipad-safari-before.webp" alt="Emulated Safari, old drawing order: plain frame only, no clones or smoke." class="_75_percent_width" /></p>

<p><em>Chromium patched to act like Safari, old drawing order: only the plain frame. Both images come from the emulation with the drawn test figure, not from a real iPad, which is why they look rough.</em></p>

<p><img src="/assets/images/2026-11-04-naruto-clones-invisible-on-ipad-safari-after.webp" alt="Emulated Safari, fixed drawing order: clones and smoke behind the drawn figure." class="_75_percent_width" /></p>

<p><em>Same patch, fixed drawing order: clones and smoke behind the figure.</em></p>

<h2 id="the-fix">The fix</h2>

<p>Never draw the video under a composite mode. <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/blob/b443c3e95538658ba9a2a0b6b5a7bc3901913106/src/clone-effect.ts#L74-L89">The fix</a> puts the video down normally and masks it afterwards with <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation#value"><code>destination-in</code></a>. The mask is a bitmap, which WebKit handles correctly:</p>

<pre><code class="language-ts">ctx.drawImage(video, 0, 0, width, height);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(mask, 0, 0, width, height);
</code></pre>

<p>The pixels should come out the same in any browser: video multiplied by mask alpha, either way.</p>

<p>The fix also logs <a href="https://github.com/Nocodile-AI/naruto-gesture-detection/blob/b443c3e95538658ba9a2a0b6b5a7bc3901913106/src/clone-effect.ts#L394-L403"><code>personCoverage</code></a>, the opaque share of the person layer, since “no errors” told me nothing here.</p>

<h2 id="the-regression-test">The regression test</h2>

<p><a href="https://github.com/Nocodile-AI/naruto-gesture-detection/blob/b443c3e95538658ba9a2a0b6b5a7bc3901913106/tests/e2e/clone-compositing.spec.ts"><code>tests/e2e/clone-compositing.spec.ts</code></a> keeps the patch in the repo, so anyone with <a href="https://playwright.dev/">Playwright</a> can run it on any OS:</p>

<pre><code class="language-sh">bun run build &amp;&amp; bunx playwright test clone-compositing
</code></pre>

<p>Claude wrote it before the fix. On the old drawing order it failed: background alpha expected 0, received 255. It passes on the new order.</p>

<h2 id="confirmation-on-the-ipad">Confirmation on the iPad</h2>

<p>The emulation comes from reading the source, so a real device still had to confirm it. A LAN dev server with a self-signed certificate came first, and my devices could not open it. So the fix went to a branch, <a href="https://developers.cloudflare.com/pages/">Cloudflare Pages</a> built it with an HTTPS URL, and I tested on the iPad. The clones and smoke appeared. Later iPad logs show <code>personCoverage</code> between 21% and 42% of the frame across four real triggers, where an opaque full frame would read 100%.</p>

<p>From the source, I expect macOS Safari and iPad Chrome and Firefox to be affected too, but I have not tested those.</p>

<h2 id="the-cost-without-ai">The cost without AI</h2>

<p>Claude reported a likely cause and a fix about 19 minutes after my first message. I confirmed it on my iPad about 45 minutes after that message. I added the diagnostics panel on 8 October to chase this failure, so this bug had stayed unsolved for almost a month.</p>

<p>Without AI, I might not have resolved it at all. Five years ago I would have needed to hire an experienced web developer who knows the Apple ecosystem and enough WebKit to go looking in the engine.</p>

<p>I give the credit for this to Opus 5.5 specifically. It could not run Apple’s renderer, but it still found the bug by reading the source, and it left a test that runs on any OS. It was really inspiring for me to see a model do that.</p>

<p>So the barrier to entry fell for me. I personally did not know how to do this work, and it used to be very expensive, but I could direct it from a Windows PC and check the result on my own iPad. Opus 5.5 also taught me: I asked how it found the bug and it wrote the whole diagnosis down for me.</p>

<p>In <a href="/blog/2025/04/11/Some-thoughts-about-AI.html">April 2025</a> I wrote that AI replaces work where examples are readily available online, and this bug fits that. WebKit’s source and the bug report had been public for years. I lacked the background to find them and connect them, and Opus 5.5 did that part.</p>

<p>(In <a href="/blog/2026/09/26/ai-is-going-to-replace-most-work-but-should-it-right-now.html">September</a> I also wrote that I feel we are over-adopting AI. On this bug AI helped me a lot, and I still need to work out how those two things fit together.)</p>

<h2 id="the-waves-of-ai-i-have-seen">The waves of AI I have seen</h2>

<ul>
  <li><a href="https://openai.com/index/hello-gpt-4o/">GPT-4o</a> felt like a big jump to me because of its image capabilities. By <a href="/blog/2025/04/11/Some-thoughts-about-AI.html">April 2025</a> I was using it to generate cover pages for reports.</li>
  <li><a href="https://openai.com/index/learning-to-reason-with-llms/">o1</a> could think and reason over complex questions.</li>
  <li><a href="https://openai.com/index/introducing-deep-research/">Deep research</a> connected models to a huge amount of internet data. In that same April post I had almost built a workflow around Gemini’s version. I don’t find it very useful nowadays.</li>
  <li>Before <a href="https://developers.openai.com/api/docs/guides/function-calling">tool calling</a>, <a href="https://www.anthropic.com/news/model-context-protocol">MCP</a> and <a href="https://agentskills.io/">agent skills</a>, AI was only plain chat.</li>
</ul>

<p>Voice input is another one. I can dictate a prompt instead of typing it: a <a href="https://en.wikipedia.org/wiki/Speech_recognition">speech recognition</a> model turns my voice into text, and the AI follows that text even when it is garbled. In this project “Stryker” came out as “STRYLER”, and Claude still followed it. It does not always work: “Cloudflare” came out as “Cloud”, and Claude copied that word into the first draft of this post.</p>

<h2 id="years-of-experience">Years of experience</h2>

<p>We still need experienced front-end and back-end developers. But a model can now read WebKit’s source for someone like me, so I am less sure what years of experience are worth. This bothers me a lot, and it comes back every time a new wave arrives.</p>

<p>I ask myself what knowledge stays valuable and what experienced developers should do now, and I don’t have a general answer. I only know what still mattered in this one bug, and what I would ask an AI for next time.</p>

<h2 id="using-ai-on-a-bug-like-this">Using AI on a bug like this</h2>

<p>None of my part needed WebKit knowledge. Claude had no iPad, so the disappearing blue dots and the test button that worked both came from me using the app on the real device. My first message about this bug was messy, but it worked because it named the device and the OS version and said exactly what appeared and what did not.</p>

<p>I also had evidence before I asked. The diagnostics log showed the render running with no errors, which moved the search from “did it draw?” to “why can’t I see it?”. The panel was worth adding, even though its test button hid the bug.</p>

<p>It helped that I knew my own setup. The LAN dev server failed on my devices, and I knew a pushed branch would get an HTTPS URL from Cloudflare Pages, so I asked for that and it got the fix onto the iPad. The emulation was only a theory from reading source until the clones appeared there.</p>

<p>When an AI gives me a cause, I want to know what it measured. The MediaPipe branch looked like the answer until a probe of real mask pixels ruled it out. I also want links to the source. I don’t understand WebKit’s internals, but I can open a link to <code>drawNativeImage</code> and check that the line says what Claude said, and the next reader or agent can check this post the same way. That is as far as I can go myself, so an AI’s answer still needs a second check from a real person who has the right knowledge.</p>

<p>Asking how it got there is useful too. The diagnosis it wrote for me covers why my test button passed and where the Apple-only code path is.</p>

<p>I ask for tests that follow the real path and measure the output. My diagnostics button passed because it skipped the broken step, and “no errors” did not mean the effect worked. A number like <code>personCoverage</code> shows the problem directly: near 100% means the person layer covers the clones and smoke. I also asked for the Chromium patch and its context to go in the repo, so anyone with Playwright can reproduce this. The test failed on the old drawing order first, and it will fail for anyone who brings that order back.</p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[naruto-gesture-detection detects a hand sign in the browser and plays a Naruto shadow clone effect over the webcam. The effect comes from naruto-shadow-clone-jutsu by Nasha Wanich. The same code works on Windows and Android. On my iPad Air (5th generation), iPadOS 27.0.1, Safari, the clones never showed up.]]></summary></entry><entry xml:lang="en"><title type="html">AI is Going to Replace Most Work, But Should It Right Now?</title><link href="https://eric15342335.github.io/blog/2026/09/26/ai-is-going-to-replace-most-work-but-should-it-right-now.html" rel="alternate" type="text/html" title="AI is Going to Replace Most Work, But Should It Right Now?" /><published>2026-09-26T00:35:20+08:00</published><updated>2026-09-26T00:35:20+08:00</updated><id>https://eric15342335.github.io/blog/2026/09/26/ai-is-going-to-replace-most-work-but-should-it-right-now</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/09/26/ai-is-going-to-replace-most-work-but-should-it-right-now.html"><![CDATA[<p>I feel like nowadays people are overhyping AI. Sure, AI can do and achieve a lot of great things. Sure, while it takes AI a lot of tokens to complete the results, the <a href="https://epoch.ai/publications/the-plunging-price-of-thought">inference cost</a> of such tokens <a href="https://artificialanalysis.ai/trends">keeps decreasing every day</a>.</p>

<p>But I still feel like nowadays people are increasingly automating or replacing workflows, stuff, or workers with AI just because they want to replace them, instead of because the act of replacing them has positive economic value (ROI) right now.</p>

<p>Sure, I agree that doing such an act in the future would be flawless, since we assume the inference cost will keep dropping, which I don’t disagree with. However, I feel like we are too aggressive about using AI to replace existing tasks, or, more appropriately, we are over-adopting AI.</p>

<p>We are increasingly building infrastructure that we might not need as much as we think we will in the future. We are also <a href="https://hai.stanford.edu/ai-index/2026-ai-index-report/economy">funding startups</a> and giving them more valuation than the value of the problems they solve, at least right now. In the future, due to <a href="https://en.wikipedia.org/wiki/Long_tail">long-tail theory</a>, those companies might be able to survive and solve such niche problems at low cost because of technological advancements.</p>

<p>That’s what I wanted to say.</p>

<p>I have never felt such a strong feeling about AI.</p>

<p>I feel like, sure, AI can replace a lot of <a href="https://www.anthropic.com/research/economic-index-june-2026-report">programmers’ manual work</a>. But is the replacement itself reasonable, valuable, mandatory, or meaningful?</p>

<p>In the future, I would confidently say yes. Right now? I remain skeptical.</p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[I feel like nowadays people are overhyping AI. Sure, AI can do and achieve a lot of great things. Sure, while it takes AI a lot of tokens to complete the results, the inference cost of such tokens keeps decreasing every day.]]></summary></entry><entry xml:lang="en"><title type="html">How to Enjoy ChatGPT Plus or Pro Unlimited Chat Quota for Agentic Coding Tasks</title><link href="https://eric15342335.github.io/blog/2026/08/03/how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks.html" rel="alternate" type="text/html" title="How to Enjoy ChatGPT Plus or Pro Unlimited Chat Quota for Agentic Coding Tasks" /><published>2026-08-03T22:45:15+08:00</published><updated>2026-08-03T22:45:15+08:00</updated><id>https://eric15342335.github.io/blog/2026/08/03/how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/08/03/how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks.html"><![CDATA[<h2 id="preface-and-background-context">Preface and Background Context</h2>

<p>We all know that:</p>

<ul>
  <li><a href="https://openai.com/codex/">Codex</a> (or <a href="https://help.openai.com/en/articles/20001275-chatgpt-work-and-codex">ChatGPT Work</a>) has a weekly quota.</li>
  <li>The weekly quota cannot sustain long-term <a href="https://openai.com/index/gpt-5-6/">GPT-5.6 Sol</a>
model usage with <code>reasoning_level=high</code>. Unless <a href="https://openai.com/">OpenAI</a>
<a href="https://codex-resets.com/">resets</a> your usage quota, you will probably run out of quota
within one or two days of eight-hour coding sessions.</li>
  <li><a href="https://chatgpt.com/">ChatGPT Chat</a> mode has an unlimited GPT-5.6 Sol High quota for
<a href="https://chatgpt.com/plans/plus/">Plus ($20)</a> or <a href="https://chatgpt.com/plans/pro/">Pro ($200)</a> plans.
    <ul>
      <li>Actually, it is not unlimited, but I have never hit that quota with four
or five concurrent prompting sessions with conversations of more than five
turns and more than ten tool-use or <a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP</a> calls.</li>
      <li>I suspect that OpenAI might have a hidden <code>/compact</code> instruction directive
after some threshold of turns is reached. However, this is just a
hypothesis.</li>
    </ul>
  </li>
  <li>The associated sandbox has no internet access and cannot install any
packages. However, it can use preinstalled packages, such as the typical
<a href="https://www.python.org/">Python</a> data-science bundle and some <a href="https://docs.npmjs.com/about-npm">npm</a> packages.</li>
  <li>You can add your own MCP server with <a href="https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt">Developer Mode</a>
enabled.</li>
  <li>The <a href="https://modelcontextprotocol.io/docs/getting-started/intro">Model Context Protocol (MCP)</a> is a way to pass information to and
from external systems, such as your <a href="https://www.cloudflare.com/learning/cloud/what-is-a-virtual-private-server/">VPS</a>.</li>
  <li>You have your own VPS with <a href="https://www.debian.org/doc/manuals/debian-reference/ch01.en.html#_the_root_user">root access</a>, unrestricted internet
access, and perhaps support for everything that <a href="https://www.docker.com/">Docker</a> needs.
    <ul>
      <li>It can be behind <a href="https://www.cloudflare.com/learning/network-layer/what-is-nat/">NAT</a> when you use
<a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/">Cloudflare Tunnel</a>. A domain name is required.</li>
    </ul>
  </li>
  <li>ChatGPT custom plugins (a.k.a. MCP) support <code>OAuth</code>, <code>Mixed</code>, and <code>None</code>
authentication methods. In my case, since I am lazy, I use the <code>None</code> method
with a long HTTP path generated by <code>openssl rand -hex 32</code> for protection. You
can also use <a href="https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/">Cloudflare Managed OAuth</a> from
<a href="https://www.cloudflare.com/zero-trust/">Cloudflare Zero Trust</a>, or deploy your own
<a href="https://modelcontextprotocol.io/specification/latest/basic/authorization">OAuth</a> server endpoint on your VPS.</li>
</ul>

<h2 id="system-architecture">System Architecture</h2>

<pre><code class="language-text">You (End User)
  -&gt; ChatGPT Chat
  -&gt; Plugins (MCP)
  -&gt; OpenAI's own infrastructure
  -&gt; Cloudflare Edge
  -&gt; Cloudflare Tunnels (Your VPS)
  -&gt; Nginx reverse proxy
  -&gt; OpenSandbox MCP server
  -&gt; OpenSandbox Server
</code></pre>

<h2 id="steps">Steps</h2>

<h3 id="vps-setup">VPS Setup</h3>

<p>The project we will use is called <a href="https://github.com/opensandbox-group/OpenSandbox">OpenSandbox</a>, which allows you
to deploy Dockerized sandboxes, with a shared Linux kernel similar to a
<a href="https://linuxcontainers.org/lxc/introduction/">Linux Container (LXC)</a>, programmatically and agentically through its MCP
support.</p>

<h4 id="checkpoints">Checkpoints</h4>

<ul>
  <li>Obtain a VPS.
    <ul>
      <li>This is not covered here, sorry! However, many VPS providers support free
trials, such as <a href="https://aws.amazon.com/free/">AWS</a> (one year), <a href="https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account">Azure</a> (one month of credits
and a one-year free-tier VM), <a href="https://cloud.google.com/free">Google Cloud</a> (three months),
<a href="https://www.digitalocean.com/try/free-trial-offer">DigitalOcean</a> (two months), <a href="https://www.vultr.com/promo/">Vultr</a> (one month), and
<a href="https://www.linode.com/lp/free-credit/">Linode</a> (two months). You can also order a
<a href="https://www.raspberrypi.com/">Raspberry Pi</a> for your home. I use this
<a href="https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/">Raspberry Pi Zero 2 W</a>, but its hardware resources
are not sufficient for multiple Dockerized applications. You can use it as
a VPS.</li>
    </ul>
  </li>
  <li>Set up basic VPS management utilities and the environment.
    <ul>
      <li>Monitoring tools (<a href="https://htop.dev/">htop</a>, <a href="https://github.com/aristocratos/btop">btop</a>, and <a href="https://github.com/fastfetch-cli/fastfetch">fastfetch</a>)
        <ul>
          <li>We all love fancy dashboards and banners!</li>
        </ul>
      </li>
      <li>File-transfer tools (<a href="https://rsync.samba.org/">rsync</a>)
        <ul>
          <li>Use these in case the vibe-coded product is genuinely useful and you want
to download it to your computer.</li>
        </ul>
      </li>
      <li>Terminal text editors (<a href="https://micro-editor.github.io/">micro</a> and <a href="https://sinelaw.github.io/fresh/">Fresh</a>)
        <ul>
          <li>These are for editing configuration files and environment keys.</li>
        </ul>
      </li>
      <li>Background process execution (<a href="https://github.com/tmux/tmux/wiki">tmux</a>)
        <ul>
          <li>This is for running the MCP server in the background.</li>
        </ul>
      </li>
      <li>
        <p>Version control (<a href="https://git-scm.com/">Git</a>)</p>

        <pre><code class="language-bash">git clone https://github.com/opensandbox-group/OpenSandbox --depth=1
</code></pre>
      </li>
      <li>Cloudflare Tunnel (<a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/">cloudflared</a>)
        <ul>
          <li>This guide does not cover the traditional method, such as an
<a href="https://nginx.org/en/docs/">Nginx</a> reverse proxy with <a href="https://www.cloudflare.com/learning/ssl/what-is-sni/">SNI</a> filtering and
<a href="https://github.com/acmesh-official/acme.sh">acme.sh</a> or <a href="https://letsencrypt.org/getting-started/">Let’s Encrypt</a> TLS certificates. :D</li>
        </ul>
      </li>
      <li>Web server (<a href="https://nginx.org/en/docs/">Nginx</a>)
        <ul>
          <li>We need Nginx for URL-path-based filtering and authentication because
ChatGPT plugins do not support a static, unchanged
<code>Authorization: Bearer</code> header.</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-nginx.webp" alt="Nginx reverse-proxy configuration" /></p>

<p><code>/etc/nginx/sites-available/default</code>:</p>

<pre><code class="language-nginx">server {
    listen 127.0.0.1:8081;

    location = /&lt;your-openssl-rand-hex-32-path&gt;/mcp {
        proxy_pass http://127.0.0.1:8000/mcp;
    }

    location / {
        return 404;
    }
}
</code></pre>

<ul>
  <li>Set up <a href="https://www.python.org/">Python</a> with <a href="https://docs.astral.sh/uv/">uv</a>.
    <ul>
      <li>Follow the <a href="https://docs.astral.sh/uv/getting-started/installation/">official uv installation guide</a>.</li>
    </ul>
  </li>
  <li>Set up <a href="https://www.docker.com/">Docker</a>.
    <ul>
      <li>Follow the <a href="https://docs.docker.com/engine/install/">official Docker Engine installation guide</a>.</li>
    </ul>
  </li>
  <li>Set up the <a href="https://github.com/opensandbox-group/OpenSandbox/tree/main#create-a-code-interpreter-and-execute-commandscodes">OpenSandbox server</a>.
    <ul>
      <li>Run <code>uv sync</code> inside <code>~/OpenSandbox/server</code>.</li>
    </ul>
  </li>
</ul>

<pre><code class="language-bash">root@OpenSandbox:~/OpenSandbox/server# uv run opensandbox-server init-config --example docker
</code></pre>

<ul>
  <li>Set up the <a href="https://github.com/opensandbox-group/OpenSandbox/tree/main/sdks/mcp/sandbox/python">OpenSandbox MCP server</a>.
    <ul>
      <li>Run <code>uv sync</code> inside <code>~/OpenSandbox/sdks/mcp/sandbox/python</code>.</li>
    </ul>
  </li>
  <li>Configure API access keys in <code>.bashrc</code> and <code>.sandbox.toml</code>.</li>
</ul>

<p>Fill in <code>[server] -&gt; api_key</code>:</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-sandbox_toml.webp" alt="OpenSandbox TOML configuration" /></p>

<p>Fill in <code>OPEN_SANDBOX_API_KEY</code> in <code>.bashrc</code> with the same value:</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-open_sandbox_api_key.webp" alt="OpenSandbox API key configuration" /></p>

<p><code>source ~/.bashrc</code>.</p>

<ul>
  <li>Deploy.</li>
</ul>

<p>Open two <code>tmux</code> sessions.</p>

<pre><code class="language-console">root@OpenSandbox:~/OpenSandbox/server# uv run opensandbox-server
</code></pre>

<pre><code class="language-console">root@OpenSandbox:~/OpenSandbox/sdks/mcp/sandbox/python# uv run opensandbox-mcp --transport streamable-http --api-key "$OPEN_SANDBOX_API_KEY"
</code></pre>

<h3 id="cloudflare-setup">Cloudflare Setup</h3>

<h4 id="cloudflare-tunnel-installation">Cloudflare Tunnel Installation</h4>

<p>Select <code>Protect and Connect -&gt; Networking -&gt; Tunnel</code>:</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-cloudflare_tunnels.webp" alt="Cloudflare Tunnel installation" /></p>

<p>Create a tunnel.</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-create_tunnel.webp" alt="Create a Cloudflare Tunnel" /></p>

<p>Follow the installation instructions to install <code>cloudflared</code> on your VPS.</p>

<p>Configure routes.</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-routes.webp" alt="Cloudflare Tunnel routes" /></p>

<p>Configure the Published Application to point to your Nginx reverse proxy.</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-published_application.webp" alt="Published Application MCP port" /></p>

<h3 id="chatgpt-setup">ChatGPT Setup</h3>

<p>First, go to <a href="https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt">Plugins</a>.</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-plugin.webp" alt="ChatGPT Plugins screen" /></p>

<p>Then, add a new plugin.</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-plugin2.webp" alt="Add a new ChatGPT plugin" /></p>

<p>Configure your plugin with the <code>None</code> authentication method, and use the long
<code>openssl rand -hex 32</code> string you generated as the HTTP path. For example:</p>

<pre><code class="language-text">https://opensandbox-mcp.example.com/b1b823517ddb64de407ba18f8766c493097b22d8c13622f2c994710332282c57/mcp
</code></pre>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-create_plugin.webp" alt="Create the ChatGPT plugin" /></p>

<p>Then, enable all permissions for the AI to use the MCP server to perform
<code>rm -rf</code> on your Dockerized sandbox.</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-elevated_risk.webp" alt="Elevated-risk permissions" /></p>

<p>Then, check your plugin profile. You should be able to see the plugin MCP tools.</p>

<p><img src="/assets/images/2026-08-03-how-to-enjoy-chatgpt-plus-or-pro-unlimited-chat-quota-for-agentic-coding-tasks-plugin_mcp_tools_desc.webp" alt="OpenSandbox MCP tools in ChatGPT" /></p>

<h3 id="usage">Usage</h3>

<p>Simply tell ChatGPT “Use OpenSandbox”. It will figure out how to best utilize it.</p>

<h3 id="maintenance">Maintenance</h3>

<p>If your VPS is low on disk space, you might want to add <code>0 10 * * * /usr/bin/docker system prune -a -f &gt;&gt; /var/log/docker-prune.log 2&gt;&amp;1</code> to your <code>crontab -e</code> to clean up Docker cached images.</p>

<h2 id="why-this-works">Why This Works</h2>

<p>Because ChatGPT Chat has a virtually unlimited quota for Plus and above users.
Although you cannot launch subagents in the harness that ChatGPT Chat uses, it
is still a very powerful GPT-5.6 Sol model with <code>reasoning_level=high</code>. I would
say it is worth it.</p>

<p>It is quite easy to get a temporary and free VPS with sufficiently powerful
resources to run concurrent, sandboxed, and isolated agentic coding tasks while
letting your agents screw up their own development environments inside the
sandbox. Plus, sandboxes expire over time, which is configurable, so it is quite
easy to manage the garbage that your agent creates. If you do not like it, just
leave it alone, and it will eventually be gone!</p>

<h2 id="appreciation">Appreciation</h2>

<p>Thanks, ChatGPT, for supporting my research on this topic.</p>

<p>Thanks to the OpenSandbox project for its agent-native sandbox runtime and
just-perfect MCP support.</p>

<p>Thanks to OpenAI for resetting my ChatGPT Codex quota and allowing virtually
unlimited ChatGPT Chat usage.</p>

<h3 id="side-note">Side Note</h3>

<p>I have used the unlimited quota to develop a to-do app. See it here: <a href="https://github.com/eric15342335/aurora-todo-app">https://github.com/eric15342335/aurora-todo-app</a></p>

<h2 id="to-do">To Do</h2>

<p>Automate this deployment through <a href="https://cloud-init.io/">cloud-init</a>, <a href="https://docs.ansible.com/ansible/latest/getting_started/index.html">Ansible</a>,
or <a href="https://developer.hashicorp.com/terraform/tutorials">Terraform</a>, although I have not used Ansible or Terraform.
Perhaps a simple <a href="https://docs.docker.com/reference/dockerfile/">Dockerfile</a> will work, too.</p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><category term="chatgpt" /><category term="codex" /><category term="mcp" /><category term="opensandbox" /><summary type="html"><![CDATA[Preface and Background Context]]></summary></entry><entry xml:lang="en"><title type="html">How to make the most of your ChatGPT $20 subscription on the Web UI</title><link href="https://eric15342335.github.io/blog/2026/07/14/how-to-make-most-use-of-chatgpt-20-subscription.html" rel="alternate" type="text/html" title="How to make the most of your ChatGPT $20 subscription on the Web UI" /><published>2026-07-14T20:29:36+08:00</published><updated>2026-07-14T20:29:36+08:00</updated><id>https://eric15342335.github.io/blog/2026/07/14/how-to-make-most-use-of-chatgpt-20-subscription</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/07/14/how-to-make-most-use-of-chatgpt-20-subscription.html"><![CDATA[<p>We often imagine an agent that can work for us continuously - researching, self-criticizing, understanding the big picture, and avoiding premature convergence.</p>

<p>Here is the simplest prompt I found to keep <a href="https://openai.com/index/gpt-5-6/">ChatGPT 5.5 or 5.6</a> working on a task for longer.</p>

<p>Copy and paste the following verbatim into the chat:</p>

<pre><code class="language-text">Spend at least [N minutes] actively working on this task before giving the final answer. Count only productive task work, not idle time, waiting, sleep commands, artificial delays, or time spent merely keeping a timer running. Use timing utilities only to measure elapsed time, and briefly report how you measured it.
</code></pre>

<p>Practically, the value of N can be set to <code>N &lt;= 30</code>. Otherwise, ChatGPT’s internal safety/alignment guardrails might trigger (it may detect time‑wasting behavior) and refuse to operate for a long time.</p>

<hr />

<p>I store my reusable prompts in my GitHub repository: <a href="https://github.com/eric15342335/misc/tree/main/prompts">eric15342335/misc - prompts</a>.</p>

<p>I am expected to graduate in July 2027 and am seeking entry-level roles in cloud computing and AI.</p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><category term="chatgpt" /><category term="prompts" /><category term="ai" /><summary type="html"><![CDATA[We often imagine an agent that can work for us continuously - researching, self-criticizing, understanding the big picture, and avoiding premature convergence.]]></summary></entry><entry xml:lang="en"><title type="html">some funny unrelated stories from my algorithm class journey</title><link href="https://eric15342335.github.io/blog/2026/07/06/algorithm-class-stories.html" rel="alternate" type="text/html" title="some funny unrelated stories from my algorithm class journey" /><published>2026-07-06T01:53:00+08:00</published><updated>2026-07-06T01:53:00+08:00</updated><id>https://eric15342335.github.io/blog/2026/07/06/algorithm-class-stories</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/07/06/algorithm-class-stories.html"><![CDATA[<p><a href="https://youtu.be/EqrszD4XYSM">https://youtu.be/EqrszD4XYSM</a></p>

<p>For context, this type of video is basically a 爽文-style animated novel recap. The male protagonist is treated as a villain by several female leads, but after his memories are exposed, everyone realizes that his “evil” actions were actually sacrifices. Later in the plot, even his death turns out to be part of saving the world. The story is exaggerated and funny, but maybe it stayed in my mind because it is also about being judged before the full path is visible.</p>

<p>This reminds me of my COMP3252 lecture stories. I remember when the professor was teaching graph algorithms like Floyd-Warshall, I was watching some videos in this style. Obviously, this was not a learning strategy I would recommend to anyone, including my past self. I cannot believe I am still watching the same type of videos today.</p>

<p>Note: Later, I realized that I had placed heavy emphasis on this course, and that binge-watching was not a long-term solution. I studied hard and spent a lot of time revising it. I do not regret getting a grade A.</p>

<p>So I agree with one quote that says: 人生有很多条路，怎么走是自己决定的，没有说哪条路算正确的，自己不后悔就行。</p>

<p>I hope that when I graduate and look back on my past blogs like this, I will not regret what I have done, or the life choices I have made.</p>

<p>Eric</p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[https://youtu.be/EqrszD4XYSM]]></summary></entry><entry xml:lang="en"><title type="html">How to check AWS Lightsail 90 days trial usage</title><link href="https://eric15342335.github.io/blog/2026/07/03/how-to-check-aws-lightsail-90-days-trial-usage.html" rel="alternate" type="text/html" title="How to check AWS Lightsail 90 days trial usage" /><published>2026-07-03T19:24:02+08:00</published><updated>2026-07-03T19:24:02+08:00</updated><id>https://eric15342335.github.io/blog/2026/07/03/how-to-check-aws-lightsail-90-days-trial-usage</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/07/03/how-to-check-aws-lightsail-90-days-trial-usage.html"><![CDATA[<p>If you want to quickly check whether your AWS Lightsail usage is still covered by the new customer 90-day free benefit, here is the simplest way I use.</p>

<h2 id="step-1">Step 1</h2>

<p>Go to <a href="https://console.aws.amazon.com/billing/home?region=us-east-1#/bills">Billing and Cost Management</a>.</p>

<h2 id="step-2">Step 2</h2>

<p><img src="/assets/images/2026-07-03-AWS-Lightsail-free-90days-1.webp" alt="Go to Bills and click Expand All" /></p>

<p>Go to <strong>Bills</strong> and click <strong>Expand All</strong>.</p>

<h2 id="step-3">Step 3</h2>

<p><img src="/assets/images/2026-07-03-AWS-Lightsail-free-90days-2.webp" alt="Lightsail resource line item showing the trial usage" /></p>

<p>Go check the <a href="https://aws.amazon.com/lightsail/">Lightsail</a> resource. The <code>$0.00 per Hrs from 0 to 750 for BundleUsage:2GB in Asia Pacific (Singapore)</code> will appear. $0 indicates your resource usage is under the new customer 90 days free benefit.</p>

<p>Alternatively, you can ask <a href="https://aws.amazon.com/q/">Amazon Q</a> to help you query the data, or help you find it.</p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[If you want to quickly check whether your AWS Lightsail usage is still covered by the new customer 90-day free benefit, here is the simplest way I use.]]></summary></entry><entry xml:lang="en"><title type="html">One click install RustNet on Debian 13 with GeoLite2 database</title><link href="https://eric15342335.github.io/blog/2026/06/28/oneclick-rustnet-debian-13-installation.html" rel="alternate" type="text/html" title="One click install RustNet on Debian 13 with GeoLite2 database" /><published>2026-06-28T14:20:00+08:00</published><updated>2026-06-28T14:20:00+08:00</updated><id>https://eric15342335.github.io/blog/2026/06/28/oneclick-rustnet-debian-13-installation</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/06/28/oneclick-rustnet-debian-13-installation.html"><![CDATA[<h2 id="background">Background</h2>

<p>As of now, <a href="https://github.com/domcyrus/rustnet">RustNet</a> is not available in the official Debian 13 repositories.</p>

<p><a href="https://github.com/domcyrus/rustnet/blob/cfab164c93c7ef8ac757d1a7847bb7aae6bf54ff/INSTALL.md#debianubuntu-deb-packages">Manual installation is required</a>, which downloads Debian packages from GitHub releases. We can call the GitHub public API to retrieve the latest release and download the Debian package automatically.</p>

<p>Also, rustnet supports <a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/">GeoLite2 database for IP geolocation</a>.</p>

<p>We can download the latest GeoLite2 database from <em>unofficial</em> GitHub releases and place it in the <code>/root</code> directory. When you log in as the root user, rustnet will automatically load the GeoLite2 database in the <code>/root</code> directory. You can put the database in any of the supported paths too.</p>

<p>(If you have a MaxMind account, you could use the tool <code>sudo apt install geoipupdate mmdb-bin</code> instead.)</p>

<pre><code class="language-sh">root@localhost:~# rustnet -h
Cross-platform network monitoring tool

Usage: rustnet [OPTIONS]

Options:
// ...
      --geoip-country &lt;PATH&gt;
          Path to GeoLite2-Country.mmdb database. Auto-discovered from: ./resources/geoip2, $XDG_DATA_HOME/rustnet/geoip, ~/.local/share/rustnet/geoip, /usr/share/GeoIP, /usr/local/share/GeoIP, /opt/homebrew/share/GeoIP, /var/lib/GeoIP
      --geoip-asn &lt;PATH&gt;
          Path to GeoLite2-ASN.mmdb database (same search paths as --geoip-country)
      --geoip-city &lt;PATH&gt;
          Path to GeoLite2-City.mmdb database (same search paths as --geoip-country; superset of Country — provides city name and postal code in addition to country)
      --no-geoip
          Disable GeoIP lookups entirely
// ...
</code></pre>

<h2 id="script">Script</h2>

<p>This script assumes the root user. Run <code>sudo su</code> first if you are not root.</p>

<pre><code class="language-sh">deb="/root/rustnet.deb"

arch="$(dpkg --print-architecture)"
case "$arch" in
amd64|arm64|armhf) ;;
*) echo "Unsupported architecture for RustNet: $arch" &gt;&amp;2; exit 1 ;;
esac

url="$(
curl -fsSL https://api.github.com/repos/domcyrus/rustnet/releases/latest \
| grep "browser_download_url" \
| grep -io "https://[^\"]*rustnet_linuxdeb_${arch}\\.deb" \
| head -n1
)"

test -n "$url"
wget -q "$url" -O "$deb"
dpkg -i "$deb" || apt-get install -f -y
rm -f "$deb"

cd /root
wget -qO- https://api.github.com/repos/P3TERX/GeoLite.mmdb/releases/latest \
| grep "browser_download_url" \
| grep -E "GeoLite2-(ASN|City|Country)\.mmdb" \
| cut -d '"' -f 4 \
| wget -qi -

rustnet
</code></pre>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[Background]]></summary></entry><entry xml:lang="en"><title type="html">How to save your HKU transcript (unofficial copy) beautifully</title><link href="https://eric15342335.github.io/blog/2026/06/28/hku-transcript-unofficial-copy.html" rel="alternate" type="text/html" title="How to save your HKU transcript (unofficial copy) beautifully" /><published>2026-06-28T13:50:00+08:00</published><updated>2026-06-28T13:50:00+08:00</updated><id>https://eric15342335.github.io/blog/2026/06/28/hku-transcript-unofficial-copy</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/06/28/hku-transcript-unofficial-copy.html"><![CDATA[<h2 id="step-1">Step 1</h2>

<p>Select “Transcript (Student Copy)”
<img src="/assets/images/2026-06-28-transcript-step1.webp" alt="HKU Portal left menu bar" /></p>

<h2 id="step-2">Step 2</h2>

<p>Right click at the empty background of the page and select “This Frame” &gt; “Print Frame…” to open the print dialog.</p>

<p>Different browser UI might vary. I used Firefox in this example.</p>

<p>The idea is to print the frame instead of the whole page, so that the header and footer of the page are not included in the printout.</p>

<p><img src="/assets/images/2026-06-28-transcript-step2.webp" alt="A firefox browser right-click context menu with &quot;This Frame&quot; selected" /></p>

<h2 id="step-3">Step 3</h2>

<p>Select “Save to PDF” as the printer and click “Print” to save the transcript as a PDF file.
<img src="/assets/images/2026-06-28-transcript-step3.webp" alt="Print Dialog of the Transcript&quot;" /></p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[Step 1]]></summary></entry><entry xml:lang="en"><title type="html">Interest</title><link href="https://eric15342335.github.io/blog/2026/04/05/interest.html" rel="alternate" type="text/html" title="Interest" /><published>2026-04-05T19:45:00+08:00</published><updated>2026-04-05T19:45:00+08:00</updated><id>https://eric15342335.github.io/blog/2026/04/05/interest</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/04/05/interest.html"><![CDATA[<p>今天研究了一些computer vision的<a href="https://www.youtube.com/watch?v=g16KvSEq0XU">玩意</a>（具体是什么不重要）。</p>

<p>然后我在想，其实我真正感兴趣的field是什么，</p>

<p>我又怎么知道我真正感兴趣的是什么呢？</p>

<p>很多时候，你感兴趣，可能是因为你没有压力，没有deadline。</p>

<p>当爱好遇上deadline，demand，<a href="https://www.scrum.org/resources/what-sprint-goal">sprint goal</a>，说不定再喜欢的爱好也会变成一种折磨。</p>

<p>不知道呢，唉</p>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[今天研究了一些computer vision的玩意（具体是什么不重要）。]]></summary></entry><entry xml:lang="en"><title type="html">OpenMP on Windows stack overflow when using large stack</title><link href="https://eric15342335.github.io/blog/2026/02/26/openmp-on-windows-stack-overflow.html" rel="alternate" type="text/html" title="OpenMP on Windows stack overflow when using large stack" /><published>2026-02-26T20:58:30+08:00</published><updated>2026-02-26T20:58:30+08:00</updated><id>https://eric15342335.github.io/blog/2026/02/26/openmp-on-windows-stack-overflow</id><content type="html" xml:base="https://eric15342335.github.io/blog/2026/02/26/openmp-on-windows-stack-overflow.html"><![CDATA[<p>On Windows, when we compile an OpenMP program with large stack usage using mingw-w64 by default, it will throw a stack overflow error (0xc00000fd):</p>

<pre><code class="language-shell">123er@eric310  /d/Personal Data/Repositories/personal-repo/APAI4013/Assignment-1
$ gcc -fopenmp task2.c -static -g
</code></pre>

<pre><code class="language-shell">123er@eric310  /d/Personal Data/Repositories/personal-repo/APAI4013/Assignment-1
$ strace ./a
--- Process 6256 created
--- Process 6256 loaded C:\Windows\System32\ntdll.dll at 00007fffbfd20000
--- Process 6256 loaded C:\Windows\System32\kernel32.dll at 00007fffbe720000
--- Process 6256 loaded C:\Windows\System32\KernelBase.dll at 00007fffbc810000
--- Process 6256 loaded C:\Windows\System32\ucrtbase.dll at 00007fffbd970000
--- Process 6256 thread 5832 created
--- Process 6256, exception c00000fd at 00007ff761779226
--- Process 6256 thread 17792 exited with status 0xc00000fd
--- Process 6256 exited with status 0xc00000fd
</code></pre>

<p>Adding the <code>-Wl,--stack,1000000000</code> flag to GCC can resolve this issue by increasing the stack size used by the program:</p>

<pre><code class="language-shell">123er@eric310  /d/Personal Data/Repositories/personal-repo/APAI4013/Assignment-1
$ gcc -fopenmp task2.c -static -g -Wl,--stack,10000000000 
</code></pre>

<pre><code class="language-shell">123er@eric310  /d/Personal Data/Repositories/personal-repo/APAI4013/Assignment-1
$ strace ./a
--- Process 20596 created
--- Process 20596 loaded C:\Windows\System32\ntdll.dll at 00007fffbfd20000
--- Process 20596 loaded C:\Windows\System32\kernel32.dll at 00007fffbe720000
--- Process 20596 loaded C:\Windows\System32\KernelBase.dll at 00007fffbc810000
--- Process 20596 loaded C:\Windows\System32\ucrtbase.dll at 00007fffbd970000
--- Process 20596 thread 1092 created
--- Process 20596 thread 5492 created
// ... more threads created
--- Process 20596 loaded C:\Windows\System32\kernel.appcore.dll at 00007fffbb4c0000
--- Process 20596 loaded C:\Windows\System32\msvcrt.dll at 00007fffbf020000
Time taken: 0.898000 seconds
41
6334
15724
24464
9961
32391
18716
19912
17673
20037
 18467
 26500  19169
 11478  29358  26962
  5705  28145  23281  16827
   491   2995  11942   4827   5436
 14604   3902    153    292  12382  17421
 19718  19895   5447  21726  14771  11538   1869
 25667  26299  17035   9894  28703  23811  31322  30333
  4664  15141   7711  28253   6868  25547  27644  32662  32757
 12859   8723   9741  27529    778  12316   3035  22190   1842    288
757147
122502946
610374658
956230507
379002193
722484005
1299818353
-1370045995
-743873488
1826067517
--- Process 20596 thread 21820 exited with status 0x0
--- Process 20596 thread 15588 exited with status 0x0
// ... more threads exited
--- Process 20596 exited with status 0x0
</code></pre>

<p>The program:</p>

<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;omp.h&gt;
#include &lt;stdlib.h&gt;

#define n 16000
int main(void) {
    int L[n][n];
    int X[n];
    int Y[n];
    double start_time, end_time;

    omp_set_num_threads(16);

    start_time = omp_get_wtime();
    // init fixed seed
    srand(1);

    // init L, X
    for (int i = 0; i &lt; n; i++) {
        X[i] = rand();
        for (int j = 0; j &lt;= i; j++) {
            L[i][j] = rand();
        }
        Y[i] = 0;
    }

    #pragma omp parallel for schedule(static)
    for (int i = 0; i &lt; n; i++) {
        for (int j = 0; j &lt;= i; j++) {
            Y[i] += L[i][j] * X[j];
        }
    }

    end_time = omp_get_wtime();
    printf("Time taken: %f seconds\n", end_time - start_time);

    // Visualize the result
    for (int i = 0; i &lt; 10; i++) {
        printf("%d\n", X[i]);
    }
    for (int i = 0; i &lt; 10; i++) {
        for (int j = 0; j &lt;= i; j++) {
            printf("%6d ", L[i][j]);
        }
        printf("\n");
    }
    for (int i = 0; i &lt; 10; i++) {
        printf("%d\n", Y[i]);
    }
    return 0;
}
</code></pre>

<h2 id="appendix">Appendix</h2>

<p>Compiler version:</p>

<pre><code class="language-shell">$ gcc -v
Using built-in specs.
COLLECT_GCC=D:\Personal Data\Repositories\mingw64\bin\gcc.exe
COLLECT_LTO_WRAPPER=D:/Personal\ Data/Repositories/mingw64/bin/../libexec/gcc/x86_64-w64-mingw32/15.2.0/lto-wrapper.exe
OFFLOAD_TARGET_NAMES=nvptx-none
Target: x86_64-w64-mingw32
Configured with: ../configure --prefix=/R/winlibs_staging_ucrt64/inst_gcc-15.2.0/share/gcc --build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --enable-offload-targets=nvptx-none --with-pkgversion='MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r5' --with-tune=generic --enable-checking=release --enable-threads=posix --disable-sjlj-exceptions --disable-libunwind-exceptions --disable-serial-configure --disable-bootstrap --enable-host-shared --enable-plugin --disable-default-ssp --disable-rpath --disable-libstdcxx-debug --disable-version-specific-runtime-libs --disable-symvers --enable-languages=c,c++,fortran,lto,objc,obj-c++ --disable-gold --disable-nls --disable-stage1-checking --disable-win32-registry --disable-multilib --enable-ld --enable-libquadmath --enable-libada --enable-libssp --enable-libstdcxx --enable-lto --enable-fully-dynamic-string --enable-libgomp --enable-graphite --enable-mingw-wildcard --enable-libstdcxx-time --enable-libstdcxx-pch --with-mpc=/c/Prog/winlibs_staging_ucrt/custombuilt64 --with-mpfr=/c/Prog/winlibs_staging_ucrt/custombuilt64 --with-gmp=/c/Prog/winlibs_staging_ucrt/custombuilt64 --with-isl=/c/Prog/winlibs_staging_ucrt/custombuilt64 --disable-libstdcxx-backtrace --enable-install-libiberty --enable-__cxa_atexit --without-included-gettext --with-diagnostics-color=auto --enable-clocale=generic --enable-libgdiagnostics --with-libiconv --with-system-zlib --with-build-sysroot=/R/winlibs_staging_ucrt64/gcc-15.2.0/build_mingw/mingw-w64 CFLAGS='-I/c/Prog/winlibs_staging_ucrt/custombuilt64/include/libdl-win32   -march=nocona -msahf -mtune=generic -O2 -Wno-error=format' CXXFLAGS='-Wno-int-conversion  -march=nocona -msahf -mtune=generic -O2' LDFLAGS='-pthread -Wl,--no-insert-timestamp -Wl,--dynamicbase -Wl,--high-entropy-va -Wl,--nxcompat -Wl,--tsaware' LD=/c/Prog/winlibs_staging_ucrt/custombuilt64/share/binutils/bin/ld.exe
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 15.2.0 (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r5)
</code></pre>

<pre><code class="language-shell">$ gcc --version
gcc.exe (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r5) 15.2.0
Copyright (C) 2025 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
</code></pre>]]></content><author><name>Cheng Ho Ming, Eric</name><email>eric310@connect.hku.hk</email></author><category term="blog" /><summary type="html"><![CDATA[On Windows, when we compile an OpenMP program with large stack usage using mingw-w64 by default, it will throw a stack overflow error (0xc00000fd):]]></summary></entry></feed>